JAVA集合类是一个特别有用的工具类,可用于存储数量不确定的对象,并可以实现常用的数据结构,如栈,队列等。除此之外集合还可用于存储具有映射关系的关联数组。
JAVA集合大致可以分为Set,List,Map,Queue四种体系,其中Set代表无序不可重复的集合;List代表有序可重复的集合,Map代表具有映射关系的集合,java5又新增了Queue,代表一种队列集合实现。如果想要访问List集合的元素,可以通过索引来访问,访问Map集合的元素,通过key值来访问value值,访问Set集合元素,只能通过元素本身来访问这也是Set集合元素不可重复的原因。
集合和数组的主要区别:
1>数组是定长的,而集合的长度可变
2>数组可以存储基本数据类型和对象,而集合只能存储对象(实际上是对象的引用,习惯上称为对象)
Collection接口和Iterator接口:
Collection接口是Set,List,Queue接口的父接口,该接口里定义的方法同样可用于操作Set,List,Queue集合。下面是一些常用的方法,更多详细的方法请参见java的API
Iterator iterator() 返回一个Iterator对象,用于遍历集合
boolean add(Object o) 向集合中添加一个元素,如果集合别添加操作改变了,则返回true
boolean addAll(Collection c) 把集合中所有元素都添加到指定集合中,如果集合被添加操作改变了,则返回true
void clear() 清除集合中的所有元素,是的集合的长度变为0
boolean remove(Object o) ; 删除集合中指定的元素,如果集合中包含多个指定元素,则只删除第一个符合条件的
boolean contains(Object o) ; 判断集合中是否包含指定元素,包含则返回true
boolean isEmpty(); 判断集合是否为空,若为空则返回true
int size() ; 返回集合元素的个数
Object[] toArray() ; 把集合转换为数组,所有的集合元素变成对应的数组元素
import java.util.*; public class CollectionTest { public static void main(String[] args) { Collection c1 = new ArrayList(); c1.add("孙悟空"); c1.add("猪八戒"); c1.add(6); //虽然集合不能存储基本数据类型,但java支持自动装箱 System.out.println(c1.size()); //3 System.out.println(c1); //[孙悟空, 猪八戒, 6],集合都实现了toString()方法 c1.remove("猪八戒"); System.out.println(c1.size()); //2 Collection c2 = new HashSet(); c2.add("孙悟空"); c2.add(66); c1.addAll(c2); System.out.println(c1); //[孙悟空, 6, 66, 孙悟空] ,元素可以重复 c2.addAll(c1); System.out.println(c2); //[6, 66, 孙悟空] ,元素不重复 System.out.println(c1.contains("猪八戒")); //false c1.removeAll(c2); System.out.println(c1); //[] 两个孙悟空都会被删除 Object[] obj = c2.toArray(); //obj.add("张三"); 变成定长数组,不能再添加元素 c2.clear(); System.out.println(c2); //[] 数组被清空 } }
虽然两个集合的实现类不同,但是当作Collection来操作时使用上面的方法并没有任何区别
Lambda表达式遍历集合:
import java.util.*; public class Iterator { public static void main(String[] args) { Collection c = new HashSet(); c.add("孙悟空"); c.add("猪八戒"); c.add("唐僧"); c.forEach(obj->System.out.println(obj)); } }
Iterator迭代器
import java.util.*; public class Iterat { public static void main(String[] args) { Collection c = new HashSet(); c.add("孙悟空"); c.add("猪八戒"); c.add("唐僧"); Iterator it = c.iterator(); //获得集合的迭代器对象 it.forEachRemaining(obj->System.out.println(obj)); //直接用java8新增的方法遍历集合元素 while(it.hasNext()) { String p = (String) it.next(); System.out.println(p); if(p == "猪八戒") { //c.remove(p); 1...将会产生异常 p = "沙和尚"; //2... 不会改变集合元素 it.remove(); } } System.out.println(c); } }
Iterator接口也是集合框架中的重要成员,它主要用于迭代访问Collection中的元素,所以Iterator的对象也被称为迭代器。Iterator依附于Collection集合,若没有Collection,Iterator将没有存在的意义
由上面的代码看出,Iterator接口提供了两种方法遍历Collection中的元素,还可以使用remove()删除迭代指针指向的元素。这里需要注意,在迭代器迭代Collection集合时,集合的元素不能被改变,只能通过迭代器的remove()方法删除元素,因为迭代器本身对这种改变是可预知的,否则将会产生运行时异常,如1....处的测试代码
再看2...处的测试代码,最终的集合输出结果并没有得到“沙和尚”这个字符串,这可以得出结论,当使用Iterator迭代访问Collection时,并不把几何元素本身传递给迭代变量,而是把集合元素的值传递给迭代变量,所以修改迭代变量的值并不会对改变集合
使用foreach遍历集合:
for (Object object : c) { System.out.println(object); //会引发异常 c.remove(object); }
使用Predicate操作集合:
public class Predica { public static void main(String[] args) { Collection books = new ArrayList(); books.add("操作系统"); books.add("剑指offer"); books.add("疯狂java讲义"); books.add("java从入门到精通"); books.removeIf(book->((String) book).length()<10); //将满足条件的都删除 System.out.println(books); } }
Predicate的主要作用是筛选符合要求的集合元素。例如需要统计书名中包含“java”的书籍;统计长度不小于10的书;统计出现“疯狂”字样的书。如果按照之前的方法去做,则要用到循环,若是分别统计三种情况,则需三次循环,很麻烦。但是使用Predicate则要方便很多
import java.util.*; import java.util.function.Predicate; public class Predica { @SuppressWarnings("all") public static void main(String[] args) { Collection books = new HashSet(); books.add(new String("操作系统")); books.add(new String("剑指offer")); books.add(new String("疯狂java讲义")); books.add(new String("java从入门到精通")); System.out.println(total(books,ele->((String) ele).length()<10)); //3 System.out.println(total(books, ele->((String) ele).contains("java"))); //2 System.out.println(total(books, ele->((String) ele).contains("疯狂"))); //1 } public static int total(Collection c , Predicate p) { int cnt = 0; for (Object object : c) { if(p.test(object)) //满足筛选条件 cnt++; } return cnt; } }
import java.util.*; import java.util.function.Predicate; public class Predica { @SuppressWarnings("all") public static void main(String[] args) { Collection books = new HashSet(); books.add(new String("操作系统")); books.add(new String("剑指offer")); books.add(new String("疯狂java讲义")); books.add(new String("java从入门到精通")); System.out.println(books.stream().filter(ele->((String) ele).length()<10).count()); System.out.println(books.stream().filter(ele->((String) ele).contains("java")).count()); System.out.println(books.stream().filter(ele->((String) ele).contains("疯狂")).count()); //附加代码用于进一步了解Stream对集合的操作 books.stream().mapToInt(ele->((String) ele).length()).forEach(obj->System.out.println(obj)); //获得集合的Stream之后,可以对集合整体进行操作 System.out.println(books.stream().count()); } }
使用Stream省去了对集合的遍历操作来判断集合元素是否满足条件,由于Stream使用较少,更多Stream的用法参考API文档。
Set集合:
Set集合就像一个罐子,不能记住元素添加的顺序,也不允许元素重复,若添加重复元素,add()方法会返回false
HashSet类:
HashSet类是Set接口典型的实现类,也是我们经常使用到的类,线程不安全,元素值可以为空,通过hash算法决定元素的存储位置,因此具有较好的存取和查找性能
下面我们来看一看HashSet判断集合元素相等的标准
import java.util.Collection; import java.util.HashSet; //A类只重写equals方法,总是返回true class A { public boolean equals(Object obj) { return true; } } //B类重写hashCode方法,总是返回相同的值 class B { public int hashCode() { return 1; } } //C类重写两个方法 class C { public boolean equals(Object obj) { return true; } public int hashCode() { return 2; //注意,这里不能再返回1,因为类C的hashCode()返回值为1,不然C的对象都添加失败 } } public class HashSetTest { @SuppressWarnings("all") public static void main(String[] args) { Collection hs = new HashSet(); hs.add(new A()); hs.add(new A()); hs.add(new B()); hs.add(new B()); System.out.println(hs.add(new C())); System.out.println(hs.add(new C())); System.out.println(hs.size()); System.out.println(hs); } }
如果要把某个类的对象保存到HashSet中,在重写类的equals()和hashCode()方法时,要尽量保证两者返回值的一致性,即equals()判断元素相等,则他们的哈希值就相等,通常equals()方法中用于比较元素相等的实例变量,都应该用于计算哈希值
如果集合中包含可变元素,若修改了用于判断相等与计算哈希值的实例变量,将导致无法正确操作集合中被修改的元素。看一个例子:
import java.util.*; class Q { int number; public Q(int number) { this.number = number; } public boolean equals(Object obj) { if(this == obj) return true; if(obj!=null&obj.getClass()==Q.class) { Q s = (Q)obj; return s.number == this.number; } return false; } public int hashCode() { return this.number; } public String toString() { return "number:"+number; } } public class HashSetTest2 { public static void main(String[] args) { Collection N = new HashSet(); N.add(new Q(-1)); N.add(new Q(0)); N.add(new Q(1)); N.add(new Q(2)); System.out.println(N); //[number:-1, number:0, number:1, number:2] Iterator it = N.iterator(); Q first = (Q) it.next(); //将对象的引用传给first first.number=2;//将第一个元素修改成与第二个元素相等的元素,equals,hashCode System.out.println(N); //[number:2, number:0, number:1, number:2] N.remove(new Q(2)); System.out.println(N); //[number:2, number:0, number:1]只删除了未被修改过的number为2的对象 System.out.println("集合中是否包含number为2的元素?"+N.contains(new Q(2))); //false N.remove(new Q(2)); System.out.println(N);//[number:2, number:0, number:1],无法删除被修改过的第一个元素 System.out.println("集合中是否包含number为-1的元素?"+N.contains(new Q(-1))); //false N.remove(new Q(-1)); //同样无法删除第一个元素 } }
LinkedHashSet类:
LInkedHashSet是HashSet的子类,它用链表维护了元素的插入顺序,因此性能略低于HashSet,但也是由于它用链表维护了内部顺序,所以在迭代访问数组元素时,效率较高。依然不允许元素重复
TreeSet类:
TreeSet是SortedSet接口的实现类,正如SortedSet名字所暗示的一样,TreeSet确保集合中的元素处于排序状态,注意是排序状态,而不是有序状态
下面是TreeSet较HashSet多出来的几种常见用法:
public class TreeSetTest { public static void main(String[] args) { SortedSet c = new TreeSet(); //此处不能再使用Collection接口 c.add(0); c.add(6); c.add(-1); c.add(4); System.out.println(c); //[-1, 0, 4, 6] 已排好序 System.out.println(c.first()); System.out.println(c.last()); System.out.println(c.headSet(2)); //[-1,0] 边界可以不是集合中的元素 不包括边界 System.out.println(c.tailSet(4)); //[4,6] 包括边界 System.out.println(c.subSet(0, 4)); //[0] 包括上边界 不包括下边界 System.out.println(((TreeSet) c).lower(0)); //比0小的第一个元素 } }
自然排序:
java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法用于比较连个元素的大小,obj1.compareTo(Object obj2),若返回值为正数,则obj1大于obj2,若为负数,则obj2大,若返回值为0,则表示相等
若试图把某一对象添加到TreeSet和中,则该对象的类必须实现Comparable接口,并将比较对象强制转换为相同类型,也就是说想TreeSet中添加的对象必须为同一个类对象
public class TreeSetTest { public static void main(String[] args) { SortedSet c = new TreeSet(); c.add(new String()); c.add(new Date()); //引发异常 } }
import java.util.*; class Person implements Comparable { int age; public Person(int age) { this.age = age; } public int compareTo(Object o) { return 1; } public boolean equals(Object obj) { return true; } public String toString() { return "age:"+age; } } public class TreeSetTest { public static void main(String[] args) { Person p = new Person(22); TreeSet ts = new TreeSet(); ts.add(p); ts.add(p); //虽然equals返回true,但是compareTo方法返回1,则会判定对象与本身都会不相等 System.out.println(ts); //[age:22, age:22]添加成功 } }
由上面代码得出结论,TreeSet判定两个对象是否相等的条件只有compareTo()方法,若方法返回值为0,则两个对象视为相等,无法重复添加。但是如果equals判断对象相等,但compareTo返回值不等于0,则违反了Set集合元素不重复的规则,若equals返回不相等,但compareTo返回值为0,则无法正确添加元素,所以要保证equals与compareTo方法的返回值一致。
与HashSet集合相同,如果修改了集合中保存的可变对象用于判断equals和compareTo的变量,将导致钙元素不可操控,所以为了程序更加健壮,不要轻易修改集合元素的关键实例变量
定制排序:
TreeSet ts = new TreeSet((o1,o2)-> { stu s1 = (stu)o1; stu s2 = (stu)o2; return s1.age>s2.age?-1:s1.age<s2.age?1:0; } );
创建TreeSet时键入Comparator比较器,可以指定排列顺序,判断两个元素是否相等的依据便是compare方法的返回值是否为0。
EnumSet类:
EnumSet是专为枚举类设计的集合类,EnumSet集合元素也是有序的,以EnumSet中枚举值在Enum类中的定义顺序来决定集合元素的顺序。且集合中的元素不能为空,否则会引发异常。
EnumSet类没有任何构造器,创建对象只能使用静态方法
import java.util.*; enum Season { spring,summer,fall,winter; } public class EnumSetTest { public static void main(String[] args) { //创建EnumSet集合,集合元素就是Session枚举类全部枚举值 EnumSet es1 = EnumSet.allOf(Season.class); System.out.println(es1); //创建空的EnumSet EnumSet es2 = EnumSet.noneOf(Season.class); System.out.println(es2); //手动添加值 es2.add(Season.spring); es2.add(Season.winter); System.out.println(es2); EnumSet es3 = EnumSet.of(Season.winter, Season.summer); System.out.println(es3); //自动调整顺序 EnumSet es4 = EnumSet.range(Season.summer, Season.winter); //这里前后顺序不能颠倒 System.out.println(es4); EnumSet es5 = EnumSet.complementOf(es4); //es4+es5=Season System.out.println(es5); } }
各Set实现类的性能分析:
HashSet的性能总是比TreeSet好,尤其是查询,添加元素等操作,由于TreeSet内部需要额外维护一个红黑树,只有当总是需要保持一个排好序的集合时,才考虑使用TreeSet
HashSet还有一个子类LinkedHashSet,它的查询和删除等操作等性能都要比HashSet差,因为它额外维护了一个链表,但是对于遍历操作,则会有更好的效率
EnumSet是所有Set实现类中性能最好的,但是它的取值有限。
List集合:
List是个有序,可重复的集合,集合中每个元素都有其对应的顺序索引,所以在Collection集合操作方法的基础上增加了一些使用索引的方法。下面列举一些常用的方法
import java.util.*; public class ListTest { @SuppressWarnings("all") public static void main(String[] args) { List list = new ArrayList(); list.add(new String("孙悟空")); list.add(new String("猪八戒")); list.add(new String("沙和尚")); System.out.println(list); //[孙悟空, 猪八戒, 沙和尚] 有序 list.add(1, new String("唐僧")); System.out.println(list); //[孙悟空, 唐僧, 猪八戒, 沙和尚]添加到索引为1的位置 System.out.println(list.get(0)); //孙悟空 list.remove(0); System.out.println(list); //[唐僧, 猪八戒, 沙和尚] list.set(1, new String("唐僧")); System.out.println(list); //[唐僧, 唐僧, 沙和尚] 元素可重复 System.out.println(list.indexOf(new String("唐僧")));//0,第一次出现时索引的位置 System.out.println(list.subList(0, 2)); //[唐僧, 唐僧],包括0,不包括2 } }
import java.util.*; class MyComparator implements Comparator { public int compare(Object o1, Object o2) { String obj1 = (String)o1; String obj2 = (String)o2; return obj1.length()>obj2.length()?1:obj1.length()<obj2.length()?-1:0; } } public class ListTest2 { @SuppressWarnings("all") public static void main(String[] args) { List books = new ArrayList(); books.add(new String("Java编程思想")); books.add(new String("Linux程序设计")); books.add(new String("鸟哥私房菜")); System.out.println(books); //[Java编程思想, Linux程序设计, 鸟哥私房菜] books.sort(new MyComparator()); System.out.println(books); //[鸟哥私房菜, Java编程思想, Linux程序设计],按长度从小到大排序 //也可以使用lambda表达式 books.sort((o1,o2)->((String)o2).length()-((String)o1).length()); //1..... System.out.println(books); //[Linux程序设计, Java编程思想, 鸟哥私房菜],反序输出 books.removeIf(ele->((String)ele).length()<8); //删除符合过滤条件的元素 //2..... System.out.println(books); books.replaceAll(ele->((String)ele).length()); //3..... System.out.println(books);//[9, 8],将元素都替换成其对应长度 } }
import java.util.*; public class ListTest3 { @SuppressWarnings("all") public static void main(String[] args) { List books = new ArrayList(); books.add(new String("Java编程思想")); books.add(new String("Linux程序设计")); books.add(new String("鸟哥私房菜")); ListIterator it = books.listIterator(); while(it.hasNext()) { System.out.println(it.next()); it.add("------分隔符-------"); //在迭代指针的位置添加字符串 } System.out.println("=========开始反向迭代========="); while(it.hasPrevious()) { System.out.println(it.previous()); it.remove(); } System.out.println(books); } }
ArrayList和Vector都是List的实现类,Vector是个比较古老的集合,早在jdk1.0的时候就存在,所以包含了很多名称很长的方法,后来让它继承了List集合,所以一些功能会有重复。这两个实现类最主要的区别就是Vector是线程安全的
而ArrayList是线程不安全的,所以ArrayList的效率要高一些。他们都封装了一个动态的,可重新分配的Object[]数组,初始长度为10(也可以指定),Vectory可以设置存储空间增加的数量,而ArrayList不可以。
Vectory还有一个子类stack,用于模拟栈,同样线程安全,性能较差,所以渐渐的被ArrayDeque所取代。
在多线程并发的环境中,可以使用Collections工具类将ArrayList包装成为线程安全的。所以Vectory很少用
固定长度的List集合:
List fixedList = (List) Arrays.asList("孙悟空","猪八戒","沙和尚"); System.out.println(fixedList); //[孙悟空, 猪八戒, 沙和尚],变成数组 fixedList.forEach(ele->System.out.println(ele)); fixedList.add("唐僧"); //异常 fixedList.remove("孙悟空"); //异常
该List集合是Arrays的内部类ArrayList的实例,固定长度,不能增加或删除元素,但是可以进行修改操作
Queue集合:
Queue用来模拟列这