Java集合之List,Set集合详细解释

数组与集合

数组和集合都是存放 Java数据的 Java容器,数组与集合底层的数据结构都是线性结构,对于数组而言,一旦声明了长度,数据存放的容量就不能进行改变,集合就不一样,集合也是容器但是里面的数据容量却是可以改变的,不同的集合所存放的集合数据规则也就不一样。数组在删除,添加操作方面的效率不太高,存储数据的特点是有序可重复的,但是对于无序,不可重复的需求来讲,数组就无法实现了,此时Java就引出了集合这个概念。Java中的集合类都放在了java.util包中。

单列集合框架结构

在这里插入图片描述

上图中的集合主要就是三种Collection,List,Set集合,List,Set接口集合继承的是Collection单体集合接口,并拥有Collection中的接口方法。Collection单体集合主要存放一个一个的对象。

  • List集合是有序可重复的,实现类主要有ArrayList, LinkedList,Vector
  • Set集合是无序不可重复,实现类主要有HashSetLinkedHashSetTreeSet

Collection接口常用方法

①添加:

add(Object obj):只要是对象都可以添加

addAll(Collection c):添加另一个集合的元素

②删除:

clear():清空集合元素

remove(Object obj)删除某个元素对象

removeAll(Collection c):删除集合元素

③判断:

isEmpty():集合是否为空

contains(Object c):是否包含某个元素

contains(Collection c):是否包含某个集合

④获取:

size():获取集合大小
Iterator<E> iterator():获取迭代器对象,遍历集合

⑤其他:

Object[] toArray():集合转数组
retainAll(Collection c):两个集合交集操作

使用这些方法:

    Collection c = new ArrayList();
    //添加集合元素
    c.add("hello");
    c.add("world");
    c.add(199);//Integer.valueOf()
    Object[] o = c.toArray();
    System.out.println(Arrays.toString(o));
    System.out.println(c.size());//3
    Collection c1 = new ArrayList();
    c1.add("hello");
    c1.add("world");
    //添加另一个集合元素
    System.out.println(c);
    c.addAll(c1);
    System.out.println(c);//[hello, world, 199, hello, world]
    //删除c集合中的hello,只会删除第一个
    System.out.println(c.remove("hello"));
    System.out.println(c);
    c.clear();
    System.out.println(c);// [] c集合已经清空 
    System.out.println(c.isEmpty());// true 是否为空 

此段代码中,199并不是对象为什么能够添加呢?因为底层使用了自动装箱的操作Integer.valueOf()的操作,注意只是打印出的集合的内容而不是地址

重点来看一下集合retainAll(Collection c)的方法,它是判断两个集合是否具有交集,使用方式就是oldCourse.retainAll(newCollection),如果oldCourse发生了改变则为true,反之false

    Collection c1 = new ArrayList();
    c1.add("world");
    c1.add("cv");
    c1.add("xx");
    c1.add("abf");

    Collection c2 = new ArrayList();
    c2.add("hello");
    c2.add("world");

    //集合取交集,判断c2是否发生了改变,改变返回true,没变返回false
    boolean b1 = c2.retainAll(c1);
    System.out.println(b1);
    System.out.println(c1);//[world, cv, xx, abf]
    System.out.println(c2);//[world]

**分析:**判断c2是否与c1有交集,发现c1与c2具有交集“world”,则c2发生了改变,输出的b1为true,c1并不会发生改变,如果没有交集的话,c2会变为空[],c1还是原始的数据。

List集合接口

常用方法
  • 增:add(Object obj)

  • 删:remove(int index)或remove(Object obj)

  • 改:set(int index, Object ele)

  • 查:get(int index):传入一个,索引值得到值

  • 插:add(int index, object ele)

  • 长度:size()

  • 遍历:① Iterator迭代器方式
    ② 增强for循环
    ③ 普通的循环

    List list = new ArrayList();
    //添加
    list.add("a");
    list.add("b");
    list.add("c");
    list.remove(0);//删除第一个元素
    list.set(0, "a");//修改第一个元素
    System.out.println(list.get(list.size()-1));//得到最后一个元素
    list.add(2, "d");//在索引位置为2的位置插入元素d
    System.out.println(list);//a,c,d

重点来看集合遍历的三种方式

  • ① Iterator迭代器遍历的方式:

    	List list = new ArrayList();
    		//添加
    		list.add("a");
    		list.add("b");
    		list.add("c");
    		Iterator it = list.iterator();
    		while(it.hasNext()) {
          
          
    			String s = (String) it.next();
    			System.out.println(s);
    		}
    

    原理:在这里插入图片描述

    图中可以看出从a开始查,有元素就next(),没有元素则停止while循环

  • ② 增强for循环实现

        List<String> list = new ArrayList<>();
        //添加
        list.add("a");
        list.add("b");
        list.add("c");
        for(String i: list) {
          
          
            System.out.println(i);		
        }
    
  • ③ 普通的循环

        List<String> list = new ArrayList<>();
        //添加
        list.add("a");
        list.add("b");
        list.add("c");
        for (int i = 0; i < list.size(); i++) {
          
          
            System.out.println(list.get(i));
        }
    

    同时我们也可以用```ListIterator``对象进行遍历:

    这是ListIterator的正向遍历

        List<String> list = new ArrayList<>();
        //添加
        list.add("a");
        list.add("b");
        list.add("c");
        //正向遍历
        ListIterator<String> lt = list.listIterator();
        while(lt.hasNext()) {
          
          
            String s = lt.next();
            System.out.println(s);
        }
    

    这是ListIterator的逆向遍历

        List<String> list = new ArrayList<>();
        //添加
        list.add("a");
        list.add("b");
        list.add("c");
        //正向遍历,如果没有这一步逆向遍历会为空,因为逆向遍历获取的是前一个元素,a的前面没有元素,所以为空
        ListIterator<String> lt = list.listIterator();
        while(lt.hasNext()) {
          
          
            lt.next();
        }
    	//逆向遍历
        while(lt.hasPrevious()) {
          
          //获取上一个元素
            String s = lt.previous();
            System.out.println(s);//输出的是cba
        }
    

ListIteratorIterator的区别:

  • ListIterator实现了Iterator,并且还可以包括其它的增加元素,替换,获取前一个或者后一个元素的索引等
  • ListIterator只能操作List,而Iterator能操作Set和List
  • ListIterator不仅可以正向遍历而且还可以逆向遍历

看一个ListIterator例子

判断集合中是否存在“java”,如果存在就在里面添加“javaee”

下面提供二种方式:

第一种方法,直接for循环遍历

    public static void main(String[] args) {
    
    
        List<String> l = new ArrayList<>();
        l.add("php");
        l.add("java");
        l.add("c");
        check(l);
    }
    //第一种方法,直接for循环遍历
    public static void check(List<String> list) {
    
    
        for(int i = 0; i<list.size(); i++) {
    
    
            if("java".equals(list.get(i))) {
    
    
                list.add("javaee");
            }
        }
        System.out.println(list);
    }

第二种方法,使用迭代器进行遍历添加

	public static void main(String[] args) {
    
    
		List<String> l = new ArrayList<>();
		l.add("php");
		l.add("java");
		l.add("c");
		check02(l);
		System.out.println(l);
	}	
	public static void check02(List<String> list) {
    
    
		ListIterator<String> li = list.listIterator();
		while(li.hasNext()) {
    
    
			String s = li.next();
			if("java".equals(s)) {
    
    
				list.add("javaee");
				break;
			}
		}
	}

如果把break去掉的话,添加并不会成功,因为添加是用list集合添加的,事先迭代器并不知道list添加了数据,这个异常是并发修改异常ConcurrentModificationException只要加个break将程序进行阻断然后再次遍历就会添加成功,当然也可以用迭代器进行遍历,添加直接交给迭代器处理,改成li.add("javaee")即可。

ArrayList

对于ArrayList.它的底层就是用数组进行实现的,因此它查询比较快,执行效率就会比较高,支持null值的存在,但是插入删除数据比较慢,还有它线程是不安全的,ArrayList继承AbstractList,实现List接口,它实现了Serializable接口,可以被序列化,实现RandomAccess实现随机访问,实现了Cloneable接口,能被克隆。

在这里插入图片描述

关于JDK7与JDK8的原码对比

JDK7:

  • 初始化时,会默认创建容量为10 的数组ArrayList list = new ArrayList();

    private static final int DEFAULT_CAPACITY = 10
    
  • 当添加的数据超出容量为10时,就会实现自动扩容,扩容默认是原来的1.5倍,同时将原来的数组复制到新扩容的数组中。

JDK8:

  • 底层Object[] elementData初始化为{}.并没创建长度为10的数组
  • 第一次调用的时候再创建容量为10的数组,后续的添加与操作和JDK7无异
LinkedList

LinkedList是双向链表,底层是用链表进行实现的,特点就是删除添加快,效率高,查询比较慢,同样它也是线程不同步,不安全的。提供方法可以访问第一个和最后一个元素。

Vector

底层使用数组进行实现的,线程是安全的,查询块,添加删除慢,效率低下。jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。在扩容方面,默认扩容为原来的数组长度的2倍。

在开发中基本不去使用,但是得知道!

特有功能:

  • addElement(E obj):添加元素

  • E elementAt(int index):指定索引位置处对应的元素值

  • Enumeration elements():获取集合所元素构成的一个枚举对象
    Enumeration里面方法
    hasMoreElements():是否更多元素
    nextElement():获取下个元素

        Vector<String> vector = new Vector<>();
        vector.add("a");
        vector.add("b");
        vector.add("c");
        //获取首元素
        System.out.println("首元素:"+ vector.elementAt(0));
        //获取集合元素构成的枚举对象
        Enumeration<String> enumeration = vector.elements();
        while(enumeration.hasMoreElements()) {
          
          
            String s = enumeration.nextElement();
            System.out.println(s);
        }
    

关于对List集合遍历的补充说明:

  • 对于实现了RandomAccess接口的List,首选for循环,后选foreach
  • 未实现RandomAccess接口的List,首选Iterator迭代器进行遍历,对于size比较大的,千万不要使用普通for循环

Queue队列

java中也提供了队列的实现方式,它是一种特殊的线性表,从一端进行插入称为入队,另一端取出数据称为出队,数据遵循先进先出(FIFO)规则,如果想要获取到后来的数据,就必须先获取到先入队的数据。

主要的成员方法:

  • boolean offer(E e):向队列末尾添加元素(入队)
  • E poll():获取并删除队列的首元素(出队)
  • E peek():获取队列的首元素

使用这些方法:

    public static void main(String[] args) {
    
    
        Queue<String> queue = new LinkedList<>();
        queue.offer("a");
        queue.offer("b");
        queue.offer("c");
        System.out.println("首元素为:"+queue.peek());//a
        System.out.println(queue.poll());//返回第一个元素并删除a
        System.out.println(queue.element());//返回第一个元素b
        for (String string : queue) {
    
    
            System.out.println(string);
        }//b,c
        //删除队列操作
        while(!queue.isEmpty()) {
    
    
            System.out.println(queue.poll());
        }
        System.out.println(queue);//[]
    }

看看add与offer,remove与poll,peek与element的区别?

  • add与offer都是表示添加,有的队列就会有大小的限制,使用add就会报出异常,使用offer只会返回false

  • 当队列为空值remove会报出异常,poll会返回null,peek与element方法也是一样的

Deque队列

属于双向队列,对于两端都可以进行相应的操作,如果只是在一端进行操作就变成了栈(先进后出)

    Deque<String> deque = new LinkedList<>();
    deque.push("a");
    deque.push("b");
    deque.push("c");
    deque.push("d");
    System.out.println("首元素为:" + deque.peekFirst());
    //移除所有元素
    while(!deque.isEmpty()) {
    
    
        deque.pop();
    }
    System.out.println(deque);
    for (String string : deque) {
    
    
        System.out.println(string);
    }		

Set集合

特点:无序,不可重复

  • 无序不代表是随机的,以HashSet为例子存储的数据并非是通过数组的索引添加,而是通过运算出哈希值进行添加的
  • 不可重复性就是保证equals方法判断不可为true,即只能添加一个元素

常用实现类

HashSet

作为Set接口的主要实现类;线程不安全的;可以存储null值,Hashset底层使用哈希表实现数据的存储的,哈希表本质就是数组,JDK1.8之前使用数组加上链表,JDK1.8之后也是数组加上链表,如果链表的元素个数大于8,此时的链表要变成红黑树(红黑二叉树的查询速度非常快)

HashSet<String> hs = new HashSet<>();
		hs.add("a");
		hs.add("ab");
		hs.add("abc");
		hs.add("abcd");
		hs.add("abc");
		hs.add("ab");
		hs.add("a")	
		Iterator<String> it = hs.iterator();
		while(it.hasNext()) {
    
    
			String s = it.next();
			System.out.println(s);
		}
		}

输出: a
ab
abc
abcd

面试题:HashSet如何解决重复的?元素添加过程:

首先,向集合中添加元素 a,通过hashCode()方法计算出哈希值,根据这个哈希值通过算法计算出HashSet底层数组位置的索引,然后判断此位置上是否有该元素:

  • 如果此位置上没有其他元素,添加成功
  • 如果有其它元素b,比较它们两个的哈希值是否一样,不一样则添加成功,如果哈希值是一样的,比较equals值,如果为true,添加不成功,如果为false,添加成功,添加的方式是以链表的方式进行添加。

使用Set时,如果添加的是对象,我们必须重写类中的hashCode()和equals方法

LinkedHashSet:

线程不安全,作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历,就是为了解决无序的HashSet,变成有序的Set

TreeSet

可以照添加对象的指定属性,按照某个规则进行排序,且元素是唯一的,主要应用在自然排序和定制排序。

自然排序用法:直接实现Comparable接口

//继承比较器接口
class Doctor implements Comparable<Doctor>{
    
    
	//属性
    String name;
	int age;  
    //构造
	public Doctor(String name, int age) {
    
    
		super();
		this.name = name;
		this.age = age;
	}
    //setter和getter
	public String getName() {
    
    
		return name;
	}
	public void setName(String name) {
    
    
		this.name = name;
	}
	public int getAge() {
    
    
		return age;
	}
	public void setAge(int age) {
    
    
		this.age = age;
	}
    //重写toString
	@Override
	public String toString() {
    
    
		return "Doctor [name=" + name + ", age=" + age + "]";
	}
    //排序的实现
	@Override
	public int compareTo(Doctor d) {
    
    
		// TODO Auto-generated method stub
		//年龄由小到大
		int num = this.age - d.age;
		//年龄相同按照姓名的字典顺序排序
		int num2 = num==0?this.name.compareTo(d.name):num;
		return num2;
	}
}
//测试
public class TreeSetDemo {
    
    
	public static void main(String[] args) {
    
    
		demo02();
	}
	private static void demo02() {
    
    
		// 自然排序:元素对应的类实现Comparable接口
		TreeSet<Doctor> ts = new TreeSet<>();
		ts.add(new Doctor("zs",12));
		ts.add(new Doctor("ls",12));
		ts.add(new Doctor("ww",12));
		ts.add(new Doctor("zs",12));
        //遍历集合
		for (Doctor doctor : ts) {
    
    
			System.out.println(doctor);
		}
	}
}

**定制排序:**通过定制排序,使用匿名内部类Comparator实现排序

class Teacher{
    
    
	String name;
	int age;
    //构造,setter,getter封装,重写toString
	public String getName() {
    
    
		return name;
	}
	public void setName(String name) {
    
    
		this.name = name;
	}
	public int getAge() {
    
    
		return age;
	}
	public void setAge(int age) {
    
    
		this.age = age;
	}
	@Override
	public String toString() {
    
    
		return "Teacher [name=" + name + ", age=" + age + "]";
	}
	public Teacher(String name, int age) {
    
    
		super();
		this.name = name;
		this.age = age;
	}
	
}
//匿名内部类实现排序
public class TreeSetDemo02 {
    
    
	public static void main(String[] args) {
    
    
		// TODO Auto-generated method stub
		//比较器
		TreeSet<Teacher> ts = new TreeSet<>(new Comparator<Teacher>() {
    
    
			@Override
			public int compare(Teacher t1, Teacher t2) {
    
    
				//按照年龄由大到最小排序
				int n = t2.age - t1.age;
				//年龄相同姓名字典排序
				int n2 = n==0?t1.name.compareTo(t2.name):n;
				return n2;
			}
		});
		ts.add(new Teacher("a", 12));
		ts.add(new Teacher("abc", 12));
		ts.add(new Teacher("f", 12));
		ts.add(new Teacher("er", 12));		
		for (Teacher teacher : ts) {
    
    
			System.out.println(teacher);
		}	
	}
}

这两种对集合进行排序的区别:

  • Comparable接口来自java.lang包使用compareTo(Object obj)方法来排序
  • comparator接口来自java.util包compare(Object obj1,Object obj2)来实现定制排序。

常见面试题:ArrayList与LinkedList的区别

  • 底层数据结构:前者为数组,后者为双向链表
  • 安全性:两者线程都是不同步,不安全
  • 插入和删除是否受元素位置的影响:前者底层是数组插入,删除元素受元素位置的影响,后者是链表实现,不受元素位置影响
  • 是否支持随机访问:前者支持随机访问(RandomAccess接口),数组天然就支持随机访问的,后者不支持
  • 内存占用:后者是双向链表,占用的空间比前者的大

可以看出,前者的效率更高

猜你喜欢

转载自blog.csdn.net/qq_41857955/article/details/107418566
今日推荐