集合
java集合大致分为5种
set:无序,不可以重复
List:有序,可以重复
Map:具有映射关系的集合
Queue:java5新增。代表一种队列集合实现。
java集合就像一个容器,把多个对像(实际上是对象的引用)丢进该容器中。
java5之前,java集合会丢失容器中所有对象的数据类型,把所有对象都当成object处理,java5增加泛型以后,java集合可以记住容器中的对象类型。
为了保存数量不确定的数据,以及保存具有映射关系的数据,java提供了集合类。集合类主要负责保存、盛装其他数据,因此集合类也叫容器类。所有集合类都位于java.util包下,为了处理多线程环境下的并发安全问题,java5还在java.util.concurrent包下提供了有一些多线程的集合类。
集合和数组的区别是数组元素可以是基本类型的值,也可以是对象(其实是保存对象的引用变量),集合只能报损对象。
java集合类主要有两个接口派生而出:Collection和Map,是java集合框架的根接口,他们有包含了一些子接口或实现。
图1 图2
图1位Collection体系集合。Set和LIst接口是Collection接口派生出的两个子接口,分别代表有序集合和无序集合。Queue是java提供的队列实现,类似于List。图2位Map体系的继承树,Map保存的每项数据都是key-value对。key是不可重复的。
两幅图中,Set,Queue,List,Map4个接口,可以分为3大类,Set集合无法记住元素顺序,因此对象不能重复,List集合像一个数组,可以记住每次添加元素的顺序,但是长度可变。Map每项数据都是键值对。
其中最常用的集合为HashSet,TreeSet,ArrayList,ArrayDeque,LinkedLIst,HashMap,TreeMap。
Collection接口是List,Set,Queue的父接口。接口里的方法可以操作以上集合。
boolean add(object o)向集合添加一个类。成功返回true
boolean addAll(Collection c)把c集合的所有元素添加到指定集合,成功返回true
void clear()清除集合所有元素。
。。。。。
所有的Collection实现类都重写了toString()方法。
传统模式下,把一个对象“丢进”集合中,集合会忘记这个对象的类型。都是object类型。java5以后,可以使用泛型来限制集合元素里的类型。
java11增加了toArray(intFunction)方法。可以返回特定类型的数组。
String[] ss=strColl.toArray(String::new);
参数是一个lambda表达式,构造器引用。
intFunction iF=a->new String[](a);
toArray(iF)
toArray(String[]::new)
Iterable接口是Collection接口的父接口。
Iterable接口在java8新增了一个forEach(Consumer action)默认方法。参数是一个函数式接口。所以在Collection集合里也可以直接调用该方法。
当使用Iterable的forEach方法遍历集合元素时,程序会依次将集合元素传给Consumer的accept(T t)方法(唯一的抽象方法),所以可以使用Lambda表达式来遍历集合元素。
Consumer是函数式接口,forEach默认方法将集合元素一一传递给consumer类型的实例。而consumer类型是lambda表达式的目标类型,因此,集合元素一一传递给了lambda表达式的形参。
consumer接口只有一个accept的抽象方法,而lambda表达式实现了这个方法,因此lambda表达式创建了一个consumer接口的实例,就是说lambda表达式的目标类型是consumer。
Iterator接口主要用于遍历集合元素,并不存元素,定义了以下四个方法。
boolean hasNext()如果被迭代的集合元素还没有被遍历完,则反回true。
object next()返回集合里上一次next方法返回的元素。
void remove 删除集合里上一次next方法返回的元素。
void forEachRemainnig(consumer action)使用lambda表达式来遍历元素集合。
Iterator必须依附于collection对象。遍历时,并不是把集合元素本身传给了迭代变量,而是把集合元素的值传给了迭代变量。
当使用它遍历时,不能改变集合里的元素。一旦在迭代过程中检测到该集合已经被修改,程序立马引发异常。可以避免共享资源而引发的潜在问题。
使用foreach循环迭代访问集合元素时,同样不能改变集合。
java8为Collection新增了removeIf(Predicate filter) Predicate是函数式接口,因此也可以用lambdda表示。
Predicate有text方法。
使用Stream操作集合
java8新增了Sream,IntStream,LongStream,DoubleSream等流式API。这些api支持串行和并行聚焦操作的元素。
有两种方法。
第一,使用Builder创建对应流的builder。
调用Builder的add方法,向流中添加元素。
调用Builder的build方法,获取对应的流。
调用流的方法。
第二种,java8允许流来操作集合。
Collection接口提供了一个默认的 Stream方法。该方法可以返回集合对应的流,接下来可以进行流式聚焦 操作。方便了集合操作,不用遍历集合。
流式聚焦操作有很多方法。其中有中间方法,即方法的返回值是另外一个流。
也有末端方法,对流的最终操作。
例如collection.stream().mapToInt(ele->((string)ele).length()).forEach(System::println)中mapToInt是中间方法,返回一个Intstream流,forEach是末端方法,遍历Intstream流。
Set集合
set集合与Collection集合差不多,只是不允许存放相同的元素。
HashSet
是Set的典型实现,大多数使用Set就是使用这个实现类。他用Hash算法来存储集合中的元素。
HashSet的特点:
不能保证元素的顺序,与添加顺序不同,顺序也可能发生变化。
不是同步的,如果多个线程同时访问一个HashSet,假设有两个或者两个以上的线程同时修改了HashSet集合时,则必须通过代码来同步
集合元素值可以是null。
当向HashSet集合中加入元素时,会调用元素的hashcode方法,得到其hash值,根据他的hash值,用算法计算出内存位置。所以,hashset可以快速查找到被检索的对象。
HasSet判断两个元素相等的方法时调用equals方法会hashcode方法,两个方法都相等,才会认为元素相等。
如果只有hashcode不等,则会放入不同的位置,根据hashcode放。
如果只有equals不等,则会放入相同的“桶”(bucket),如果多个元素的hashcode相等,则桶里会有多个元素,会导致性能下降。
当向hashset中添加可变对象时,必须十分小心,如果修改hashset集合中的对象,有可能导致该对象与集合中的其他对象相等,从而导致hashset无法准确访问该对象。
LinkedHashSet是hashset子集,它同样是根据hashcode值来决定元素存储的位置,但它同时使用链表来维护元素的次序,这样使得元素看起来以插入的顺序保存。因为需要维护插入顺序,所以性能略低于hashset,但是迭代访问set里全部元素时,有很好的性能。虽然有链表记录集合元素的添加顺序,但是仍然不允许重复元素。
TreeSet是sortedset接口的实现类,可以自动排序。因此有另外的方法。
import java.util.TreeSet; public class TreeSetTest { public static void main(String[] args) { var str=new TreeSet(); str.add("你好"); str.add("早上好"); str.add("中午好"); str.add("晚上"); str.add("中间"); str.add("早啊"); System.out.println(str); System.out.println(str.first()); System.out.println(str.last()); System.out.println(str.lower("早上好")); System.out.println(str.higher("中午好")); System.out.println(str.subSet("中午好","早上好")); System.out.println(str.tailSet("早上好")); System.out.println(str.headSet("晚上")); } } [中午好, 中间, 你好, 早上好, 早啊, 晚上] 中午好 晚上 你好 中间 [中午好, 中间, 你好] [早上好, 早啊, 晚上] [中午好, 中间, 你好, 早上好, 早啊]
Hashset采用hash算法来决定元素的存储位置的不同,TreeSet采用红黑树的数据结构来存储集合元素。
TreeSet支持两种排序:自然排序和定制排序。
自然排序
默认情况下,调用集合元素的compareto(object obj)方法·来比较元素直接的大小,然后将集合元素按升序排列。
这个方法时Comparable接口的方法,java的一些常用类都实现了该接口。比如上面的String。如果用一个没有实现该方法的类的对象,放入TreeSet中,则会出现错误。
同样的,该方法比较时,会将obj强制转换成同一类型的对象,因此当向TreeSet中添加不同类型的对象时,也会爆异常。
当吧一个对象加入Treeset集合中时,调用该对象的compareto方法,与集合中其他对象比较大小,然后根据红黑树结构找到他的存储位置,如果比较大小后相等,则无法添加到Treeset中。
如果两个对象通过compareto比较方法和equals方法不一致时会很麻烦,因为若前者相等,则不会让元素添加进去。这就和set集合的规则发生冲突。
如果向Treeset中添加了可变对象,并且之后程序修改了对象引用的实例变量。那么集合不会改变他们的顺序,并且比较方法返回0。
因此与hashset类似,如果存入可变对象的时候,尽量不要改版这些对象的实例,因为处理这些对象时,会非常复杂,而且容易出错。
定制排序
要想实现定制排序,需要通过Comparator接口的帮助,该接口是函数式接口,将该接口对象与集合关联,让该对象负责集合元素的排序逻辑。注意与集合元素类实现的Comparable接口是两回事。
当集合实现了定制排序后,集合元素可以不实现Comparable接口。
EnumSet
EnumSet是一个专为枚举类设计的集合,集合元素也是有序的,按照在Enum类内的定义顺序来决定集合元素的顺序。
EnumSet在内部以位向量的形式存储,非常紧凑,高效,因此处理速度非常快。
不允许加入null元素。但可以删除判断null元素。
没有构造器。必须调用类方法创建对象。
当复制一个collection集合里的元素来创建EnumSet集合时,必须保证collection集合里的所有元素都是同一个枚举类的枚举值。
hashset和treeset比较,hashset的性能较好,因为后者要额外的红黑树算法保证顺序。只有当需要顺序的集合时,才用treeset
linkedhashset比hashset性能不好、但是遍历较快。
EnumSet是所有set里最好的。但只能保存同一个枚举类的枚举值。
hashset,treeset,enumset都是线程不安全的。
List集合
List集合代表一个元素有序,可重复的集合,集合中每个元素都有对应的顺序索引。默认按元素的添加顺序设置索引。
import java.util.ArrayList; public class ListTest { public static void main(String[] args) { var books=new ArrayList(); books.add("好好学习"); books.add("天天向上"); books.add("团结一心"); books.add("攻抗疫情"); books.add(1,new String("好好工作")); for(int i=0;i<books.size();i++){ System.out.println(books.get(i)); } books.remove(0); System.out.println(books); System.out.println(books.indexOf(new String("好好工作"))); books.set(0,new String("锻炼身体")); System.out.println(books.subList(0,2)); } 好好学习 好好工作 天天向上 团结一心 攻抗疫情 [好好工作, 天天向上, 团结一心, 攻抗疫情] 0 [锻炼身体, 天天向上]
这里可以看出,对于两个对象,list判断相等时根据equals来判断的,两个对象,同样是“好好工作”,对于list来说,是一样的。
java8为list集合增加了sort和replaceAll两个常用的默认方法。sort方法需要一个comparator对象来控制排序。因为是函数式接口,所以可以用lambda表达式作为参数。
replace方法则需要一个UnaryOperator来替换所有集合元素。同样也是一个函数式接口。
LIst额外提供了一个listIterator方法,返回一个ListIterator对象继承了Iterator接口,提供了专门操作list的方法。增加了几个方法。增加了向前迭代和add方法。
ArryList和Vector实现类
两者都是list类的典型实现,完全支持上面功能。
都是基于数组实现的list类。封装了一个动态的允许再分配的object【】数组。,使用initialCapacity来设置数组长丢,当超出长度时,会自动增加。
当大量添加元素时,可以使用ensureCapacity()来一次性增加长度·,减少分配次数,提高性能。
如果创建空的,则默认长度为10.
还可以调用trimToSize方法,调整数组 长度为当前元素个数,节省空间。
ArryList和Vector几乎完全 相同,vector是java1.0就有了,还没有集合框架,因此有名字很长的方法,在2的时候,加入了框架,将vector改为list接口的实现,所以有了短的方法,其实没有区别。但是实际上vector有很多区别,尽量少用。
两者最重要的区别是,ArryList是线程不安全的。vector是线程安全的。因此vector性能比前者底,但是即使使用线程安全,也不建议使用vector。
stack集合继承了vector,同样是古老的集合。模拟栈,进出栈都是object元素,因此需要类型转换。 peek返回栈顶,但是不pop,pop返回栈顶并出栈。push入栈。尽量少使用,用ArryDeque替代。
固定长度list
数组deArrys提供了一个asList方法,该方法将一个数组或指定个数的对象转换为一个LIst集合,是Arry的一个内部类Arry.ArryList。该集合只能访问遍历。不能增加删除。
Queue集合
queue集合模拟队列。先进先出。新元素插入尾部,访问元素返回头部。不允许随机访问。
add指定元素加入尾部。
offer指定元素加入尾部,容量有限制时,比add好。
element获取头部元素,不删除。
peek获取头部,若为空,返回null
poll返回头部,并删除。
remove获取头部并删除。
queue有一个PriorityQueue实现类,还有一个Deque接口。Deque代表一个双端队列,可以从两端来添加删除元素。可以当做队列和栈用。
Deque有两个实现类,ArryDeque和Linkedlist。
PriorityQueue类,并不是完全按照先进先出的原则,他为元素进行了排序。类似有TreeSet有两种方式排序。不允许插入null。若没有实现排序的函数式接口,不允许插入没有实现比较接口的类的实例。
Deque接口与Arraydeque实现类。
deque双端队列,即可用作队列也可用作栈。
arraydeque是基于数组的双端队列。创建时可以指定长度。若不指定则默认16
arraydeque和ArrayList一样,底层采用动态的,可以重新分配的object[]数组来存储集合元素。
linkedList
既是list接口的实现类又是queue的实现类。因此既可以根据索引来随机访问又可以当做队列或者栈使用。
linkedlist是一个非常强大的类。
linkelist内部是链表形式来保存集合元素美因茨随机访问性能差,但是插入,删除元素性能好,ArryList和ArryDeque都是内部数组实现的。随机访问性能好。vector因为实现了线程同步功能因此性能都比较差。
对于基于数组集合实现,使用随机访问性能比使用iterator迭代访问的性能好,因为随机访问会被映射成对数组元素的访问。
LIst是一个线性表接口,ArrayList是数组实现,linkedlist是链表线性表。queue代表队列
大部分时候ArrayList性能比linkedlist好。
对于遍历,ArrayList和vector应该用随机访问get来遍历。对于linkedlist应该采用Iterator来便利。
如果要经常插入,删除来改变包含大量数据的list集合的大小,可以考虑使用linkedlist。
如果有多个线程同时访问list集合的元素,开发者考虑使用Collections将集合包装成线程安全的集合。