JAVA中级教程 - 第三讲 集合框架(上)

JAVA基础教程

学习JAVA网站 : how2j

第三讲 集合框架 - ArrayList

3.1 与数组的区别

  • 使用数组的局限性

如果要存放多个对象,可以使用数组,但是数组有局限性: 比如 声明长度是10的数组,不用的数组就浪费了,超过10的个数,又放不下。

package collection;
import charactor.Hero;
public class TestCollection {
    public static void main(String[] args) {
        //数组的局限性
        Hero heros[] = new Hero[10];
        //声明长度是10的数组
        //不用的数组就浪费了
        //超过10的个数,又放不下
        heros[0] = new Hero("盖伦");	//放不下要报错
        heros[20] = new Hero("提莫");
    }
}
复制代码
  • 重写了toString的Hero
package charactor;
 
public class Hero {
    public String name;
    public float hp;
    public int damage;
 
    public Hero() { }
    // 增加一个初始化name的构造方法
    public Hero(String name) {
        this.name = name;
    }
    // 重写toString方法
    public String toString() {
        return name;
    }
}
复制代码
  • ArrayList存放对象

为了解决数组的局限性,引入容器类的概念。 最常见的容器类就是ArrayList。 容器的容量"capacity"会随着对象的增加,自动增长;只需要不断往容器里增加英雄即可,不用担心会出现数组的边界问题。

package collection;

import java.util.ArrayList;
import charactor.Hero;
 
public class TestCollection {
    @SuppressWarnings("rawtypes")
    public static void main(String[] args) {
        //容器类ArrayList,用于存放对象
        ArrayList heros = new ArrayList();
        
        heros.add( new Hero("盖伦"));
        System.out.println(heros.size());
        
        //容器的容量"capacity"会随着对象的增加,自动增长
        //只需要不断往容器里增加英雄即可,不用担心会出现数组的边界问题。
        heros.add( new Hero("提莫"));
        System.out.println(heros.size());
    }  
}

复制代码

3.2 常用方法

image.png

  • 增加

add 有两种用法 第一种是直接add对象,把对象加在最后面

heros.add(new Hero("hero " + i));
复制代码

第二种是在指定位置加对象

heros.add(3, specialHero);
复制代码
public class TestCollection {
    public static void main(String[] args) {
        ArrayList heros = new ArrayList();
 
        // 把5个对象加入到ArrayList中
        for (int i = 0; i < 5; i++) {
            heros.add(new Hero("hero " + i));
        }
        System.out.println(heros);
 
        // 在指定位置增加对象
        Hero specialHero = new Hero("special hero");
        heros.add(3, specialHero);
 
        System.out.println(heros.toString());
    }
}
复制代码
  • 判断是否存在

通过方法contains 判断一个对象是否在容器中 判断标准: 是否是同一个对象,而不是name是否相同

ArrayList heros = new ArrayList();
Hero specialHero = new Hero("special hero");

heros.add(specialHero);
复制代码
  • 获取指定位置的对象

通过get获取指定位置的对象,如果输入的下标越界,一样会报错。

heros.get(5)
复制代码
  • 获取对象所处的位置

indexOf用于判断一个对象在ArrayList中所处的位置 与contains一样,判断标准是对象是否相同,而非对象的name值是否相等

heros.indexOf(specialHero)
复制代码
  • 删除

remove用于把对象从ArrayList中删除

//remove可以根据下标删除ArrayList的元素。
heros.remove(2);
//也可以根据对象删除
heros.remove(specialHero);
复制代码
  • 替换

set用于替换指定位置的元素

heros.set(5, new Hero("hero 5")
复制代码
  • 获取大小

size 用于获取ArrayList的大小

System.out.println(heros.size());
复制代码
  • 转换为数组

toArray可以把一个ArrayList对象转换为数组。 需要注意的是,如果要转换为一个Hero数组,那么需要传递一个Hero数组类型的对象给toArray(),这样toArray方法才知道,你希望转换为哪种类型的数组,否则只能转换为Object数组

public class TestCollection {
    public static void main(String[] args) {
        ArrayList heros = new ArrayList();
 
        // 初始化5个对象
        for (int i = 0; i < 5; i++) {
            heros.add(new Hero("hero " + i));
        }
        Hero specialHero = new Hero("special hero");
        heros.add(specialHero);
        System.out.println(heros);
        Hero hs[] = (Hero[])heros.toArray(new Hero[]{});
        System.out.println("数组:" +hs);
 
    }
}
复制代码
  • 把另一个容器所有对象都加进来

addAll 把另一个容器所有对象都加进来

heros.addAll(anotherHeros);
复制代码
  • 清空

clear 清空一个ArrayList

heros.clear();
复制代码

3.3 List接口

  • ArrayList和List

ArrayList实现了接口List,常见的写法会把引用声明为接口List类型

public static void main(String[] args) {
        //ArrayList实现了接口List
         
        //常见的写法会把引用声明为接口List类型
        //注意:是java.util.List,而不是java.awt.List
        //接口引用指向子类对象(多态)
         
        List heros = new ArrayList();
        heros.add(new Hero("盖伦"));
        System.out.println(heros.size());  
    }
复制代码
  • List接口的方法

因为ArrayList实现了List接口,所以List接口的方法ArrayList都实现了。 在ArrayList 常用方法章节有详细的讲解,在此不作赘述

3.4 泛型Generic

不指定泛型的容器,可以存放任何类型的元素;指定了泛型的容器,只能存放指定类型的元素以及其子类。

Item.java

package property;
 
public class Item {
    String name;
    int price;
     
    public Item(){   }
     
    //提供一个初始化name的构造方法
    public Item(String name){
        this.name = name;
    }
     
    public void effect(){
        System.out.println("物品使用后,可以有效果");
    } 
}
复制代码

TestCollection.java

	 	//对于不使用泛型的容器,可以往里面放英雄,也可以往里面放物品
        List heros = new ArrayList();
        heros.add(new Hero("盖伦"));
        //本来用于存放英雄的容器,现在也可以存放物品了
        heros.add(new Item("冰杖"));

		//引入泛型Generic
        //声明容器的时候,就指定了这种容器,只能放Hero,放其他的就会出错
        List<Hero> genericheros = new ArrayList<Hero>();
        genericheros.add(new Hero("盖伦"));
        //如果不是Hero类型,根本就放不进去
        //genericheros.add(new Item("冰杖"));
          
        //除此之外,还能存放Hero的子类
        genericheros.add(new APHero());
		//并且在取出数据的时候,不需要再进行转型了,因为里面肯定是放的Hero或者其子类
        Hero h = genericheros.get(0);
复制代码
  • 泛型的简写

为了不使编译器出现警告,需要前后都使用泛型,像这样:

List<Hero> genericheros = new ArrayList<Hero>();
复制代码
  • 练习-支持泛型的ArrayList

借助泛型和前面学习的面向对象的知识,设计一个ArrayList,使得这个ArrayList里,又可以放Hero,又可以放Item,但是除了这两种对象,其他的对象都不能放

@SuppressWarnings({ "serial", "rawtypes" })
public class newArrayList extends ArrayList
{
    @SuppressWarnings("unchecked")
    public boolean add(Object o) {
        if (o instanceof Hero || o instanceof Item) {
            super.add(o);
            return true;
        }
        else {
            System.out.println(o.getClass().getName() + "非Hero和Item以及它们的子类,容器不能承放!");
            return false;
        }
    }  
}

List h = new newArrayList();
复制代码

3.5 遍历

  • 用for循环遍历
for (int i = 0; i < heros.size(); i++) {
            Hero h = heros.get(i);
            System.out.println(h);
        }
复制代码
  • 迭代器遍历

image.png

使用迭代器Iterator遍历集合中的元素

		//第二种遍历,使用迭代器
        System.out.println("--------使用while的iterator-------");
        Iterator<Hero> it= heros.iterator();
        //从最开始的位置判断"下一个"位置是否有数据
        //如果有就通过next取出来,并且把指针向下移动
        //直到"下一个"位置没有数据
        while(it.hasNext()){
            Hero h = it.next();
            System.out.println(h);
        }

        //迭代器的for写法
        System.out.println("--------使用for的iterator-------");
        for (Iterator<Hero> iterator = heros.iterator(); iterator.hasNext();) {
            Hero hero = (Hero) iterator.next();
            System.out.println(hero);
        }
复制代码
  • 用增强型for循环

使用增强型for循环可以非常方便的遍历ArrayList中的元素,这是很多开发人员的首选。

不过增强型for循环也有不足: 1)无法用来进行ArrayList的初始化 2)无法得知当前是第几个元素了,当需要只打印单数元素的时候,就做不到了。 必须再自定下标变量。

	    // 第三种,增强型for循环
        System.out.println("--------增强型for循环-------");
        for (Hero h : heros) {
            System.out.println(h);
        }
复制代码

第三讲 集合框架 - 其他集合

3.6 LinkedList

序列分先进先出FIFO,先进后出FILO;FIFO在Java中又叫Queue 队列;FILO在Java中又叫Stack 栈。

  • LinkedList 与 List接口

ArrayList一样,LinkedList也实现了List接口,诸如add,remove,contains等等方法。 详细使用,请参考 ArrayList 常用方法,在此不作赘述。

接下来要讲的是LinkedList的一些特别的地方。

  • 双向链表 - Deque

除了实现了List接口外,LinkedList还实现了双向链表结构Deque,可以很方便的在头尾插入删除数据。

什么是链表结构: 与数组结构相比较,数组结构,就好像是电影院,每个位置都有标示,每个位置之间的间隔都是一样的。 而链表就相当于佛珠,每个珠子,只连接前一个和后一个,不用关心除此之外的其他佛珠在哪里。

image.png

		//LinkedList是一个双向链表结构的list
        LinkedList<Hero> ll =new LinkedList<Hero>();
		 
        //所以可以很方便的在头部和尾部插入数据
        //在最后插入新的英雄
        ll.addLast(new Hero("hero1"));
        ll.addLast(new Hero("hero2"));
        ll.addLast(new Hero("hero3"));
        System.out.println(ll);
         
        //在最前面插入新的英雄
        ll.addFirst(new Hero("heroX"));
        System.out.println(ll);
         
        //查看最前面的英雄
        System.out.println(ll.getFirst());
        //查看最后面的英雄
        System.out.println(ll.getLast());
         
        //查看不会导致英雄被删除
        System.out.println(ll);
        //取出最前面的英雄
        System.out.println(ll.removeFirst());
         
        //取出最后面的英雄
        System.out.println(ll.removeLast());
         
        //取出会导致英雄被删除
        System.out.println(ll);
复制代码
  • 队列 - Queue

LinkedList 除了实现了List和Deque外,还实现了Queue接口(队列)。 Queue是先进先出队列 FIFO,常用方法: offer 在最后添加元素 poll 取出第一个元素 peek 查看第一个元素

		//和ArrayList一样,LinkedList也实现了List接口
        List ll =new LinkedList<Hero>();
          
        //所不同的是LinkedList还实现了Deque,进而又实现了Queue这个接口
        //Queue代表FIFO 先进先出的队列
        Queue<Hero> q= new LinkedList<Hero>();
          
        //加在队列的最后面
        System.out.print("初始化队列:\t");
        q.offer(new Hero("Hero1"));
        q.offer(new Hero("Hero2"));
        q.offer(new Hero("Hero3"));
        q.offer(new Hero("Hero4"));
          
        System.out.println(q);
        System.out.print("把第一个元素取poll()出来:\t");

        //取出第一个Hero,FIFO 先进先出
        Hero h = q.poll();
        System.out.println(h);
        System.out.print("取出第一个元素之后的队列:\t");
        System.out.println(q);
          
        //把第一个拿出来看一看,但是不取出来
        h=q.peek();
        System.out.print("查看peek()第一个元素:\t");
        System.out.println(h);
        System.out.print("查看并不会导致第一个元素被取出来:\t");
        System.out.println(q);
复制代码

3.7 HashMap

  • HashMap的键值对

HashMap储存数据的方式是—— 键值对

package collection;
import java.util.HashMap;
   
public class TestCollection {
    public static void main(String[] args) {
        HashMap<String,String> dictionary = new HashMap<>();
        dictionary.put("adc", "物理英雄");
        dictionary.put("apc", "魔法英雄");
        dictionary.put("t", "坦克");
         
        System.out.println(dictionary.get("t"));
    }
}
复制代码
  • 键不能重复,值可以重复

对于HashMap而言,key是唯一的,不可以重复的。 所以,以相同的key 把不同的value插入到 Map中会导致旧元素被覆盖,只留下最后插入的元素。 不过,同一个对象可以作为值插入到map中,只要对应的key不一样

package collection;
import java.util.HashMap;
import charactor.Hero;
  
public class TestCollection {
    public static void main(String[] args) {
        HashMap<String,Hero> heroMap = new HashMap<String,Hero>();
         
        heroMap.put("gareen", new Hero("gareen1"));
        System.out.println(heroMap);
         
        //key为gareen已经有value了,再以gareen作为key放入数据,会导致原英雄,被覆盖
        //不会增加新的元素到Map中
        heroMap.put("gareen", new Hero("gareen2"));
        System.out.println(heroMap);
         
        //清空map
        heroMap.clear();
        Hero gareen = new Hero("gareen");
         
        //同一个对象可以作为值插入到map中,只要对应的key不一样
        heroMap.put("hero1", gareen);
        heroMap.put("hero2", gareen);
         
        System.out.println(heroMap);  
    }
}
复制代码
  • 练习-查找内容性能比较

准备一个ArrayList其中存放3000000(三百万个)Hero对象,其名称是随机的,格式是hero-[4位随机数] hero-3229 hero-6232 hero-9365 ...

因为总数很大,所以几乎每种都有重复,把名字叫做 hero-5555的所有对象找出来 要求使用两种办法来寻找

  1. 不使用HashMap,直接使用for循环找出来,并统计花费的时间
  2. 借助HashMap,找出结果,并统计花费的时间
public class TestCollection {
    public static void main(String[] args) {
        ArrayList<Hero> heros = new ArrayList<Hero>();
        Random r = new Random();

        //不适用hashmap 用for找出并统计时间
        long start = System.currentTimeMillis();

        for (int i = 0;i < 3000000;i++){
            String name = "hero-" + (r.nextInt(9999-1000) + 1000);
            heros.add(new Hero(name));
        }

        int sum = 0;
        for (Hero hero:heros){
            if (hero.name.equals("hero-5555"))
                sum++;
        }
        long end = System.currentTimeMillis();
        System.out.println("string总共耗时了:"+(end-start)+"ms,hero-5555共有:" + sum + "个。");
    }
}
复制代码

string总共耗时了:1965ms,hero-5555共有:350个。

ublic class TestCollection {
    public static void main(String[] args) {
        ArrayList<Hero> heros = new ArrayList<Hero>();
        Random r = new Random();
        HashMap<String,Integer> num = new HashMap<String, Integer>();

        //不适用hashmap 用for找出并统计时间
        long start = System.currentTimeMillis();

        for (int i = 0;i < 3000000;i++){
            String name = "hero-" + (r.nextInt(9999-1000) + 1000);
            heros.add(new Hero(name));
            if (num.get(name) == null)
                num.put(name,0);
            else
                num.put(name,num.get(name) + 1);
        }

        int sum = num.get("hero-5555");
        long end = System.currentTimeMillis();
        System.out.println("string总共耗时了:"+(end-start)+"ms,hero-5555共有:" + sum + "个。");
    }
}
复制代码

string总共耗时了:3181ms,hero-5555共有:330个。

3.8 HashSet

  • 元素不能重复

Set中的元素,不能重复:

package collection;
  
import java.util.HashSet;
  
public class TestCollection {
    public static void main(String[] args) {
         
        HashSet<String> names = new HashSet<String>();
         
        names.add("gareen");
         
        System.out.println(names);
         
        //第二次插入同样的数据,是插不进去的,容器中只会保留一个
        names.add("gareen");
        System.out.println(names);
    }
}
复制代码
  • 没有顺序

Set中的元素,没有顺序。严格的说,是没有按照元素的插入顺序排列。

HashSet的具体顺序,既不是按照插入顺序,也不是按照hashcode的顺序。关于hashcode有专门的章节讲解: hashcode 原理

以下是HashSet源代码中的部分注释

 /**
 * It makes no guarantees as to the iteration order of the set; 
 * in particular, it does not guarantee that the order will remain constant over time. 
*/
复制代码

不保证Set的迭代顺序; 确切的说,在不同条件下,元素的顺序都有可能不一样。

换句话说,同样是插入0-9到HashSet中, 在JVM的不同版本中,看到的顺序都是不一样的。 所以在开发的时候,不能依赖于某种臆测的顺序,这个顺序本身是不稳定的

public static void main(String[] args) {
        HashSet<Integer> numbers = new HashSet<Integer>();
 
        numbers.add(9);
        numbers.add(5);
        numbers.add(1);
 
        // Set中的元素排列,不是按照插入顺序
        System.out.println(numbers);
 
    }
复制代码
  • 遍历

Set不提供get()来获取指定位置的元素,所以遍历需要用到迭代器,或者增强型for循环。

package collection;
  
import java.util.HashSet;
import java.util.Iterator;
  
public class TestCollection {
    public static void main(String[] args) {
        HashSet<Integer> numbers = new HashSet<Integer>();
         
        for (int i = 0; i < 20; i++) {
            numbers.add(i);
        }
         
        //Set不提供get方法来获取指定位置的元素
        //numbers.get(0)
         
        //遍历Set可以采用迭代器iterator
        for (Iterator<Integer> iterator = numbers.iterator(); iterator.hasNext();) {
            Integer i = (Integer) iterator.next();
            System.out.println(i);
        }
         
        //或者采用增强型for循环
        for (Integer i : numbers) {
            System.out.println(i);
        }
         
    }
}
复制代码
  • HashSet和HashMap的关系

通过观察HashSet的源代码(如何查看源代码) 可以发现HashSet自身并没有独立的实现,而是在里面封装了一个Map. HashSet是作为Map的key而存在的 而value是一个命名为PRESENT的static的Object对象,因为是一个类属性,所以只会有一个。

private static final Object PRESENT = new Object();
复制代码
public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    //HashSet里封装了一个HashMap
    private  HashMap<E,Object> map;
 
    private static final Object PRESENT = new Object();
 
    //HashSet的构造方法初始化这个HashMap
    public HashSet() {
        map = new HashMap<E,Object>();
    }
 
    //向HashSet中增加元素,其实就是把该元素作为key,增加到Map中
    //value是PRESENT,静态,final的对象,所有的HashSet都使用这么同一个对象
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
 
    //HashSet的size就是map的size
    public int size() {
        return map.size();
    }
 
    //清空Set就是清空Map
    public void clear() {
        map.clear();
    }
     
    //迭代Set,就是把Map的键拿出来迭代
    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }
 
}
复制代码

3.9 Collection

  • Collection是一个接口

Collection是 Set List Queue和 Deque的接口 Queue: 先进先出队列 Deque: 双向链表

**注:**Collection和Map之间没有关系,Collection是放一个一个对象的,Map 是放键值对的 **注:**Deque 继承 Queue,间接的继承了 Collection

image.png

3.10 Collections

Collections是一个类,容器的工具类,就如同Arrays是数组的工具类

image.png

  • 反转

reverse 使List中的数据发生翻转

//初始化集合numbers
List<Integer> numbers = new ArrayList<>();

Collections.reverse(numbers);
复制代码
  • 混淆

shuffle 混淆List中数据的顺序

//初始化集合numbers
List<Integer> numbers = new ArrayList<>();

Collections.shuffle(numbers);
复制代码
  • 排序

sort 对List中的数据进行排序

//初始化集合numbers
List<Integer> numbers = new ArrayList<>();

Collections.sort(numbers);
复制代码
  • 交换
//初始化集合numbers
List<Integer> numbers = new ArrayList<>();

Collections.swap(numbers,0,5);
复制代码
  • 滚动

rotate 把List中的数据,向右滚动指定单位的长度

//初始化集合numbers
List<Integer> numbers = new ArrayList<>();

Collections.rotate(numbers,2);
复制代码
  • 线程安全化

synchronizedList 把非线程安全的List转换为线程安全的List。 因为截至目前为止,还没有学习线程安全的内容,暂时不展开。 线程安全的内容将在多线程章节展开。

 List<Integer> numbers = new ArrayList<>();

System.out.println("把非线程安全的List转换为线程安全的List");
List<Integer> synchronizedNumbers = (List<Integer>) Collections.synchronizedList(numbers);
复制代码

猜你喜欢

转载自juejin.im/post/7040473620593770503