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的步骤如下:

  1. 使用Stream或XXXStream的builder()类方法创建该Stream对应的Builder
  2. 重复调用Builder的add()方法向该流中添加多个元素
  3. 调用Builder的build()方法获取对应的Stream
  4. 调用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被修改过的原因。

posted @ 2020-04-24 10:45  春刀c  阅读(1148)  评论(0编辑  收藏  举报