Java8下的集合操作
使用Lambda表达式遍历集合
Java8为Iterable接口新增了一个forEach(Consumer action)默认方法,该方法所需参数的类型是一个函数式接口,而Iterable接口是一个Collection接口的父接口,因此Collection集合也可以直接调用该方法。
当程序调用Iterable的forEach(Consumer action)遍历集合元素时,程序会依次将集合元素传给Consumer的accept(T t)方法(该接口中唯一的抽象方法)。正因为Consumer是函数式接口,因此可以使用Lambda表达式来遍历集合元素。
public class CollectionEach {
public static void main(String[] args) {
//创建一个集合
Collection books = new HashSet();
books.add("三国演义");
books.add("水浒传");
books.add("红楼梦");
//使用forEach()方法遍历
books.forEach(obj-> System.out.println("迭代集合元素:"+obj));
}
}
使用Java8增强的Iterator遍历集合元素
Iterator接口也是Java集合框架的成员,但它与Collection系列、Map系列的集合不一样:Collection系列集合、Map系列集合主要用于盛装其他对象,而Iterator则用于遍历Collection集合中的元素,Iterator也被称为迭代器。
Iterator接口隐藏了各种Collection实现类的底层细节,向应用程序提供了遍历Collection集合元素的统一编程接口。Iterator接口里定义了如下4个方法:
- boolean hasNext():如果被迭代的集合元素还没有被遍历完,则返回true
- Object next():返回集合里的下一个元素
- void remove():删除集合里上一次next方法返回的元素
- void forEachRemaining(Consumer action):这是Java8为Iterator新增的默认方法,该方法可以使用Lambda表达式来遍历集合元素
//创建一个集合
Collection books = new HashSet();
books.add("三国演义");
books.add("水浒传");
books.add("红楼梦");
//获取books集合对应的迭代器
Iterator it = books.iterator();
it.forEachRemaining(System.out::println);
运行结果如下:
水浒传
三国演义
红楼梦
下面程序示范了通过Iterator接口来遍历集合元素。
public class IteratorTest {
public static void main(String[] args) {
//创建一个集合
Collection books = new HashSet();
books.add("三国演义");
books.add("水浒传");
books.add("红楼梦");
//获取books集合对应的迭代器
Iterator it = books.iterator();
// it.forEachRemaining(System.out::println);
while (it.hasNext()) {
//it.next()方法返回的数据类型是Object类型,因此需要强制类型转换
String book = (String) it.next();
System.out.println(book);
if (book.equals("红楼梦")) {
//从集合中删除上一次next()方法返回的元素
it.remove();
}
//对book变量赋值,不会改变集合元素本身
book="测试";
}
System.out.println(books);
}
}
从上面代码可看出,Iterator仅用于遍历集合,Iterator本身不具有盛装对线的能力。如果需要创建Iterator对象,则必须有一个被迭代的集合。没有集合的Iterator仿佛无本之木,没有存在的价值。
上面book="测试";
对迭代变量book进行赋值,但当再次输出books集合时,会看到集合里的元素没有任何改变。这就可以得到一个结论:当使用Iterator对集合元素进行迭代时,Iterator并不是把集合元素本身传给了迭代变量,而是把集合元素的值传给了迭代变量,所以修改迭代变量的值对集合元素本身没有任何影响。
当使用Iterator迭代访问Collection集合元素时,Collection集合里的元素不能被改变,只有通过Iterator的remove()方法删除上一次next()方法返回的集合元素才可以;否则将会发生Exception in thread "main" java.util.ConcurrentModificationException
异常
//创建一个集合
Collection books = new HashSet();
books.add("三国演义");
books.add("水浒传");
books.add("红楼梦");
//获取books集合对应的迭代器
Iterator it = books.iterator();
// it.forEachRemaining(System.out::println);
while (it.hasNext()) {
//it.next()方法返回的数据类型是Object类型,因此需要强制类型转换
String book = (String) it.next();
System.out.println(book);
if (book.equals("三国演义")) {
//从集合中删除上一次next()方法返回的元素
// it.remove();
books.remove(book);
}
//对book变量赋值,不会改变集合元素本身
book="测试";
}
上面books.remove(book);
代码位于Iterator迭代块内,也就是在Iterator迭代Collection集合过程中修改了Collection集合,所以程序将在运行时引发异常。
Iterator迭代器采用的是快速失败(fail-fast)机制,一旦在迭代过程中检测到该集合已经被修改(通常是程序中的其他线程修改),程序立即引发java.util.ConcurrentModificationException
异常,而不是显示修改后的结果,这样可以避免共享资源而引发的潜在问题。
注意:上面的程序如果改为删除"红楼梦"字符串,则不会引发异常,这样可能有些读者会"心存侥幸"地想:在迭代时好像也可以删除集合元素啊。实际上这是一种危险的行为:对于HashSet以及后面的ArrayList等,迭代时删除元素都会导致异常——只有在删除集合中的某个特定元素时才不会抛出异常,这是由集合类的实现代码决定的,程序员不应该这么做。
使用foreach循环遍历集合元素
除了使用Iterator接口迭代访问Collection集合里的元素之外,使用Java5提供的foreach循环迭代访问集合元素更加便捷。
public class ForeachTest {
public static void main(String[] args) {
//创建一个集合
Collection books = new HashSet();
books.add("三国演义");
books.add("水浒传");
books.add("红楼梦");
for (Object obj:books) {
//此处的book变量也不是集合元素本身
String book = (String)obj;
System.out.println(book);
if (book.equals("三国演义")){
//引发java.util.ConcurrentModificationException异常
books.remove(book);
}
}
System.out.println(books);
}
}
与使用Iterator接口迭代访问集合元素类似的事,foreach循环中的迭代变量也不是集合元素本身,系统只是依次把集合元素的值赋给迭代变量,因此在foreach循环中修改迭代变量的值也没有任何实际意义。
同样,当使用foreach循环迭代访问集合元素时,该集合也不能被改变,否则将引发ConcurrentModificationException异常。
使用Java8新增的Predicate操作集合
Java8为Collection集合新增了一个removeIf(Predicate filter)方法,该方法将会批量删除符合filter条件的所有元素。该方法需要一个Predicate(谓词)对象作为参数,Predicate也是函数式接口,因此可以使用Lambda表达式作为参数。
下面程序示范了使用Predicate来过滤集合
public class PredicateTest {
public static void main(String[] args) {
//创建一个集合
Collection books = new HashSet<>();
books.add("轻量级Java EE企业应用实战");
books.add("疯狂Java讲义");
books.add("疯狂Ios讲义");
books.add("疯狂Ajax讲义");
books.add("疯狂Android讲义");
//使用Lambda表达式(目标类型是Predicate)过滤集合
books.removeIf(ele->((String)ele).length()<10);
System.out.println(books);
}
}
上面books.removeIf(ele->((String)ele).length()<10);
调用了Collection集合的removeIf()方法批量删除集合中符合条件的元素,程序传入一个Lambda表达式作为过滤条件:所有长度小于10的字符串元素都会被删除。运行结果是
[疯狂Android讲义, 轻量级Java EE企业应用实战]
使用Predicate可以充分简化集合的运算,假设依然有上面程序所示的books集合,如果程序有如下三个统计需求:
- 统计书名中出现"疯狂"字符串的图书数量
- 统计书名中出现"Java"字符串的图书数量
- 统计书名长度大于10的图书数量
如果采用传统的编程方式来完成这些需求,则需要执行三次循环,但采用Predicate只需要一个方法即可。如下:
public class PredicateTest1 {
public static void main(String[] args) {
//创建一个集合
Collection books = new HashSet<>();
books.add("轻量级Java EE企业应用实战");
books.add("疯狂Java讲义");
books.add("疯狂Ios讲义");
books.add("疯狂Ajax讲义");
books.add("疯狂Android讲义");
//统计书名包含"疯狂"子串的图书数量
System.out.println(calAll(books,ele->((String)ele).contains("疯狂")));
//统计书名包含"Java"子串的图书数量
System.out.println(calAll(books,ele->((String)ele).contains("Java")));
//统计书名字符串长度大于10的图书数量
System.out.println(calAll(books,ele->((String)ele).length()>10));
}
public static int calAll(Collection books, Predicate p){
int total =0;
for (Object obj:books) {
//使用Predicate的test()方法判断该对象是否满足Predicate指定的条件
if (p.test(obj)){
total++;
}
}
return total;
}
}
上面程序先定义了一个calAll()方法,该方法将会使用Predicate判断每个集合元素是否符合特定条件——该条件通过Predicate参数动态传入。
使用Java8新增的Stream操作集合
Java8还新增了Stream、InStream、LongStream、DoubleStream等流式API,这些API代表多个支持串行和并行聚集操作的元素。上面4个接口中,Stream是一个通用的流接口,而IntStream、LongStream、DoubleStream则代表元素类型为int、long、double的流。
Java8还为上面每个流式API提供了对应的Builder,例如Stream.Builder、IntStream.Builder、LongStream.Builder、DoubleStream.Builer,开发者可以通过这些Builder来创建对应的流。
独立使用Stream的步骤如下:
- 使用Stream或XXXStream的builder()类方法创建该Stream对应的Builder
- 重复调用Builder的add()方法向该流中添加多个元素
- 调用Builder的build()方法获取对应的Stream
- 调用Stream的聚集方法
在上面4个步骤中,第4步可以根据具体需求来调用不同的方法,Stream提供了大量的聚集方法供用户调用,具体可参考Stream或XXXStream的API文档。对于大部分聚集方法而言,每个Stream只能执行一次。
public class IntStreamTest {
public static void main(String[] args) {
IntStream is = IntStream.builder().add(20).add(13).add(-2).add(18).build();
//下面调用聚集方法的代码每次只能执行一行
System.out.println("is所有元素的最大值:"+is.max().getAsInt());
System.out.println("is所有元素的最小值:"+is.min().getAsInt());
System.out.println("is所有元素的总和:"+is.sum());
System.out.println("is所有元素的总数:"+is.count());
System.out.println("is所有元素的平均值:"+is.average());
System.out.println("is所有元素的平方是否都大于20:"+is.allMatch(ele->ele*ele>20));
System.out.println("is是否包含任一元素的平方大于20:"+is.anyMatch(ele->ele*ele>20));
//将is映射成一个新的Stream,新的Stream的每个元素是原Stream元素的2倍+1
IntStream newIs = is.map(ele -> ele * 2 + 1);
//使用方法引用的方式来遍历集合元素
newIs.forEach(System.out::println);
}
}
如上面代码中的很多行都使用了is,这样就会产生Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
。所以上面的使用is的代码,每次只能执行一行,要把其他使用is的代码注释掉。
除此之外,Java8允许使用流式API来操作集合,Collection接口提供了一个Stream()默认方法,该方法可返回该集合对应的流,接下来即可通过流式API来操作集合元素。由于Stream可以对集合元素进行整体的聚集操作,因此Stream极大地丰富了集合的功能。
例如,上一个知识点的示例程序中,该程序需要额外定义一个calAll()方法来遍历集合元素,然后依次对每个集合元素进行判断——这太麻烦了。如果使用Stream,即可直接对集合中所有元素进行批量操作。下面使用Stream来改写这个程序:
public class CollectionStream {
public static void main(String[] args) {
//创建一个集合
Collection books = new HashSet<>();
books.add("轻量级Java EE企业应用实战");
books.add("疯狂Java讲义");
books.add("疯狂Ios讲义");
books.add("疯狂Ajax讲义");
books.add("疯狂Android讲义");
//统计书名包含"疯狂"子串的图书数量
System.out.println(books.stream().filter(ele->((String)ele).contains("疯狂")).count());
//统计书名包含"Java"子串的图书数量
System.out.println(books.stream().filter(ele->((String)ele).contains("Java")).count());
//统计书名字符串长度大于10的图书数量
System.out.println(books.stream().filter(ele->((String)ele).length()>10).count());
//先调用Collection对象的Stream()方法将集合转化为Stream
//再调用Stream的mapToInt()方法获取原有的Stream对应的IntStream
//调用forEach()方法遍历IntStream中每个元素
System.out.println("集合中每个元素的长度:");
books.stream().mapToInt(ele->((String)ele).length()).forEach(System.out::println);
}
}
通过上面的代码可以看出,程序只要调用Collection的Stream()方法即可返回该结合对应的Stream,接下来就可通过Stream提供的方法对所有集合元素进行处理,这样大大地简化了集合编程的代码,这也是Stream编程带来的优势。
Java8改进的List接口和ListIterator接口
Java8为List集合增加了sort()和replaceAll()两个常用的默认方法,其中sort()方法需要一个Comparator对象来控制元素排序,程序可以使用Lambda表达式来作为参数:而replaceAll()方法则需要一个UnaryOperator来替换所有集合元素,UnaryOperator也是一个函数式接口,因此程序也可以使用Lambda表达式作为参数。
public class ListTest {
public static void main(String[] args) {
List books = new ArrayList();
//向books集合中添加4个元素
books.add("轻量级Java EE企业应用实战");
books.add("疯狂Java讲义");
books.add("疯狂Ios讲义");
books.add("疯狂Ajax讲义");
books.add("疯狂Android讲义");
//使用目标类型为Comparator的Lambda表达式对List集合排序
books.sort((o1,o2)->((String)o1).length()-((String)o2).length());
System.out.println(books);
//使用目标类型为UnaryOperator的Lambda表达式来替换集合中所有元素
//该Lambda表达式控制使用每个字符串的长度作为新的集合元素
books.replaceAll(ele->((String)ele).length());
System.out.println(books);
}
}
运行结果:
[疯狂Ios讲义, 疯狂Java讲义, 疯狂Ajax讲义, 疯狂Android讲义, 轻量级Java EE企业应用实战]
[7, 8, 8, 11, 16]
上面程序中books.sort((o1,o2)->((String)o1).length()-((String)o2).length());
控制对List集合进行排序,传给sort()方法的Lambda表达式指定的排序规则是:字符串长度越大,字符串越大,因此执行完该代码后,List集合汇总的字符串会按由短到长的顺序排列。
books.replaceAll(ele->((String)ele).length());
传给replaceAll()方法的Lambda表达式指定了替换集合元素的规则:直接用集合元素(字符串)的长度作为新的集合元素。
与Set只提供一个iterator()方法不同,List还额外提供了一个listIterator()方法,该方法返回一个ListIterator对象,ListIterator接口继承了Iterator接口,提供了专门操作List的方法。ListIterator接口在Iterator接口基础上增加了如下方法:
- boolean hasPrevious():返回该迭代器关联的集合是否还有上一个元素
- Object previous():返回该迭代器的上一个元素
- void add(Object o):在指定位置插入一个元素
拿ListIterator与普通的Iterator进行对比,不难发现ListIterator增加了向前迭代的功能(Iterator只能向后迭代),而且ListIterator还可通过add()方法向List集合中添加元素(Iterator只能删除元素)。下面示范ListIterator的用法:
public class ListIteratorTest {
public static void main(String[] args) {
String[] books = {"疯狂Java讲义","疯狂ios讲义","轻量级Java EE企业应用实战"};
List bookList = new ArrayList();
for (int i = 0; i < books.length; i++) {
bookList.add(books[i]);
}
ListIterator lit = bookList.listIterator();
while (lit.hasNext()) {
System.out.println(lit.next());
lit.add("---------分隔符---------");
}
System.out.println("===========下面开始反向迭代=====");
while (lit.hasPrevious()) {
System.out.println(lit.previous());
}
}
}
从上面程序中可以看出,使用ListIterator迭代List集合时,开始也需要采用正向迭代,即先使用next()方法进行迭代,在迭代过程中可以使用add()方法向上一次迭代元素的后面添加一个新元素。运行结果:
疯狂Java讲义
疯狂ios讲义
轻量级Java EE企业应用实战
===========下面开始反向迭代=====
---------分隔符---------
轻量级Java EE企业应用实战
---------分隔符---------
疯狂ios讲义
---------分隔符---------
疯狂Java讲义
Java8为Map新增的方法
Java8除了为Map增加remove(Object key,Object value)默认方法外,还增加了如下方法:
public class MapTest {
public static void main(String[] args) {
Map map = new HashMap();
//存放多个键值对
map.put("疯狂Java讲义",109);
map.put("疯狂IOS讲义",99);
map.put("疯狂Ajax讲义",79);
//尝试替换key为"疯狂xml讲义"的value,由于原map中没有对应的key
//因此map没有改变,不会添加新的键值对
map.replace("疯狂xml讲义",66);
System.out.println(map);
//使用原value与传入参数计算出来的结果覆盖原有的value
map.merge("疯狂IOS讲义",10,(oldVal,param)->(Integer)oldVal+(Integer)param);
System.out.println(map);
//当key为"java"对应的value为null时,使用计算的结果作为新value
map.computeIfAbsent("java",(key)->((String)key).length());
System.out.println(map);
//当key为"java"对应的value存在时,使用计算的结果作为新value
map.computeIfPresent("java",(key,value)->(Integer)value*(Integer)value);
System.out.println(map);
}
}
运行结果:
{疯狂Ajax讲义=79, 疯狂IOS讲义=99, 疯狂Java讲义=109}
{疯狂Ajax讲义=79, 疯狂IOS讲义=109, 疯狂Java讲义=109}
{疯狂Ajax讲义=79, java=4, 疯狂IOS讲义=109, 疯狂Java讲义=109}
{疯狂Ajax讲义=79, java=16, 疯狂IOS讲义=109, 疯狂Java讲义=109}
Java8改进的HashMap和Hashtable实现类
HashMap和Hashtable都是Map接口的典型实现类,它们之间的关系完全类似于ArrayList和Vector的关系:Hashtable是一个古老的Map实现类,它从JDK1.0就有了,当它出现时,Java还没有提供Map接口。
Java8改进了HashMap的实现,使用HashMap存在key冲突时依然具有较好的性能。
除此之外,Hashtable与HashMap存在两点典型区别。
- Hashtable是一个线程安全的Map实现,但HashMap是线程不安全的实现,所以HashMap比HashTable的性能高一点;但如果有多个线程访问同一个Map对象时,使用HashTable实现类会更好
- Hashtable不允许使用null作为key和value,如果视图把null值放进Hashtable中,会出现空指针异常;但HashMap可以使用null作为key或value
由于HashMap里的key不能重复,所以HashMap里最多只有一个key-value对的key为null,但可以有无数多个key-value对的value为null。下面程序示范了用null值作为HashMap的key和value的情形。
public class NullInHashMap {
public static void main(String[] args) {
HashMap hm = new HashMap();
//试图将两个可以为null值的key-value对放入HashMap中
hm.put(null,null);
hm.put(null,null);
//将一个value为null值的key-value对放入HashMap中
hm.put("a",null);
//输出map
System.out.println(hm);
}
}
执行结果:
{null=null, a=null}
上面程序试图向HashMap中放入三个键值对,其中第二个hm.put(null,null);
无法放入,因为map中已经有一个键值对的key为null值,所以无法再放入key为null的键值对。hm.put("a",null);
可以放入键值对,因为一个HashMap中可以有多个value为null值。
为了成功地在HashMap、Hashtable中存储、获取对象,用作key的对象必须实现hashCode()方法和equals()方法。
与HashSet集合不能保证元素的顺序一样,HashMap、Hashtable也不能保证其中key-value对的顺序。类似于HashSet,HashMap、Hashtable判断两个key相等的标准也是:两个key通过equals()方法比较返回true,两个key的hashCode值也相等。
除此之外,HashMap、Hashtable中还包含一个containsValue()方法,用于判断是否包含指定的value。那么HashMap、Hashtable如何判断两个value相等呢?HashMap、Hashtable判断两个value相等的标准更简单:只要两个对象通过equals()方法比较返回true即可。下面程序示范了Hashtable判断两个可以相等的标准和两个value相等的标准。
public class HashtableTest {
public static void main(String[] args) {
Hashtable ht = new Hashtable();
ht.put(new A(60000),"疯狂Java讲义");
ht.put(new A(89898),"轻量级Java EE企业应用实战");
ht.put(new A(1234),new B());
System.out.println(ht);
//只要两个对象通过equals()方法比较返回true
//Hashtable就认为它们是相等的value
//由于Hashtable中有一个B对象
//它与任何对象通过equals()方法比较都相等,所以下面输出true
System.out.println(ht.containsValue("测试字符串"));
//只要两个A对象的count相等,它们通过equals()方法比较返回true,且hashCode值相等
//Hashtable即认为它们是相同的key,所以下面输出true
System.out.println(ht.containsKey(new A(89898)));
//下面语句可以删除最后一个键值对
ht.remove(new A(1234));
System.out.println(ht);
}
}
class A{
int count;
public A(int count){
this.count=count;
}
//根据count的值来判断两个对象是否相等
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj!=null && obj.getClass() ==A.class){
A a = (A)obj;
return this.count == a.count;
}
return false;
}
//根据count来计算hashCode值
@Override
public int hashCode() {
return this.count;
}
}
class B {
//重写equals()方法,B对象与任何对象通过equals()方法比较都返回true
@Override
public boolean equals(Object obj) {
return true;
}
}
运行结果:
{com.tianhao.luo.collection.map.A@15f2a=轻量级Java EE企业应用实战, com.tianhao.luo.collection.map.A@ea60=疯狂Java讲义, com.tianhao.luo.collection.map.A@4d2=com.tianhao.luo.collection.map.B@4554617c}
true
true
{com.tianhao.luo.collection.map.A@15f2a=轻量级Java EE企业应用实战, com.tianhao.luo.collection.map.A@ea60=疯狂Java讲义}
上面程序定义了A类和B类,其中A类判断两个A对象相等的标准是count实例变量:只要两个A对象的count变量相等,则通过equals()方法比较它们返回true,它们的hashCode值也相等;而B对象则可以与任何对象相等。
Hashtable判断value相等的标准是:value与另外一个对象通过equals()方法比较返回true即可。上面程序中的ht对象包含了一个B对象,它与任何对象通过equals()方法比较总是返回true,所以第一个输出判断为true。在这种情况下,不管传给ht对象的containsValue()方法参数是什么,程序总是返回true。
根据Hashtable判断两个key相等的标准,程序在第二个输出判断也是true,因为两个A对象虽然不是同一个对象,但它们通过equals()方法比较返回true,且hashCode值相等,Hashtable即认为它们是同一个key。类似的是,程序在最后ht.remove(new A(1234));
可以删除对应的键值对。
与HashSet类似的是,如果使用可变对象作为HashMap、Hashtable的key,并且程序修改了作为key的可变对象,则也可能出现与HashSet类似的情形:程序再也无法准确访问到map中被修改过的key。
public class HashMapErrorTest {
public static void main(String[] args) {
HashMap ht = new HashMap();
//此处的A类与前一个程序的A类是同一个类
ht.put(new A(60000),"疯狂Java讲义");
ht.put(new A(87563),"轻量级Java EE企业应用实战");
//获得Hashtable的key Set集合对应的Iterator迭代器
Iterator it = ht.keySet().iterator();
//取出Map中第一个key,并修改它的count值
A first = (A)it.next();
first.count=87563;
System.out.println(ht);
ht.remove(new A(87563));
System.out.println(ht);
//无法获取剩下的value,下面两行代码都将输出null
System.out.println(ht.get(new A(87563)));
System.out.println(ht.get(new A(60000)));
}
}
该程序使用的还是上一个程序定义的A类实例作为key,而A对象是可变对象。当用first.count=87563;
修改了A对象之后,实际上修改了HashMap集合中元素的key,这就导致该key不能被准确访问。当程序试图删除count为87653的A对象时,只能删除没有被修改的key所对应的key-value对。程序最后都不能访问"疯狂Java讲义"字符串,这都是因为它对应的key被修改过的原因。