目录

Java-Collection笔记

目录

前言

java中的集合主要分为三种类型:

  • Set(集)
  • List(列表)
  • Map(映射)

List

数组:几乎所有集合实现的底层都有数据的身影存在,因此我们首先需要了解一下数组。以下这段话摘自《Thinking In Algorithm》,感觉很不错现在拿出来跟大家分享。

./1.jpg

《Thinking In Algorithm》之数组

集合:接下来是集合,同样我们摘自网络上的一段解释,很不错也通俗易懂,与大家分享:

./2.jpg

集合与数组

总结一下上面两段话:

数组的大小是固定不变的,并且同一个数组只能存储相同类型的数据,该数据类型可以是基本类型也可以是引用类型。Java中集合可以存储操作不同类型和大小不固定的数据,但是Java中集合只能存储引用类型,不能存储基本类型。


ArrayList初始化的4种方法

参考

1、Arrays.asList

1
ArrayList<Type> obj = new ArrayList<Type>(Arrays.asList(Object o1, Object o2, Object o3, ....so on));

2、生成匿名内部内进行初始化

1
2
3
4
5
6
ArrayList<T> obj = new ArrayList<T>() {{
    add(Object o1);
    add(Object o2);
    ...
    ...
}};

3、常规方式

1
2
3
4
5
ArrayList<T> obj = new ArrayList<T>();
obj.add("o1");
obj.add("o2");
...
...

或者

1
2
3
ArrayList<T> obj = new ArrayList<T>();
List list = Arrays.asList("o1","o2",...);
obj.addAll(list);

4、Collections.ncopies

1
2
ArrayList<T> obj = new ArrayList<T>(Collections.nCopies(count,element));
//把element复制count次填入ArrayList中

测试代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 交集
List<String> listA_01 = new ArrayList<String>(){{
    add("A");
    add("B");
}};
List<String> listB_01 = new ArrayList<String>(){{
    add("B");
    add("C");
}};
listA_01.retainAll(listB_01);
System.out.println(listA_01); // 结果:[B]
System.out.println(listB_01); // 结果:[B, C]

// 差集
List<String> listA_02 = new ArrayList<String>(){{
    add("A");
    add("B");
}};
List<String> listB_02 = new ArrayList<String>(){{
    add("B");
    add("C");
}};
listA_02.removeAll(listB_02);
System.out.println(listA_02); // 结果:[A]
System.out.println(listB_02); // 结果:[B, C]

// 并集
List<String> listA_03 = new ArrayList<String>(){{
    add("A");
    add("B");
}};
List<String> listB_03 = new ArrayList<String>(){{
    add("B");
    add("C");
}};
listA_03.removeAll(listB_03);
listA_03.addAll(listB_03);
System.out.println(listA_03); // 结果:[A, B, C]
System.out.println(listB_03); // 结果:[B, C]

List数据去重的五种方法

方案一:借助Set的特性进行去重

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 	/**
     * 去除重复数据
     * 由于Set的无序性,不会保持原来顺序
     * @param list
     */
    public static List<String> list distinct(List<String> list) {
        final boolean sta = null != list && list.size() > 0;
        List doubleList= new ArrayList();
        if (sta) {
            Set set = new HashSet();
            set.addAll(list);
            doubleList.addAll(set);
        }
        return doubleList;
    }

方案二 : 利用set集合特性保持顺序一致去重

1
2
3
4
5
6
7
// Set去重并保持原先顺序的两种方法
   public static void delRepeat(List<String> list) {
   	   //方法一
       List<String> listNew = new ArrayList<String>(new TreeSet<String>(list));
       //方法二
       List<String> listNew2 = new ArrayList<String>(new LinkedHashSet<String>(list));
   }

方案三 : 使用list自身方法remove()–>不推荐

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
    /**
     * 去除重复数据(一般不推荐)
     * 类似于冒泡排序思想
     * @param list
     */
  public static List<Map<String, Object>> distinct(List<Map<String, Object>> list) {
        if (null != list && list.size() > 0) {
        //循环list集合
            for  ( int  i  =   0 ; i  <  list.size()  -   1 ; i ++ )  {
                for  ( int  j  =  list.size()  -   1 ; j  >  i; j -- )  {
                	// 这里是对象的比较,如果去重条件不一样,在这里修改即可
                    if  (list.get(j).equals(list.get(i)))  {
                        list.remove(j);
                    }
                }
            }
        }
        //得到最新移除重复元素的list
        return list;
    }

方案四 : 遍历List集合,将元素添加到另一个List集合中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 遍历后判断赋给另一个list集合,保持原来顺序
public static List<String> delRepeat(List<String> list) {
	  List<String> listNew = new ArrayList<String>();
	  for (String str : list) {
	       if (!listNew.contains(str)) {
	           listNew.add(str);
	       }
	   }
	  return listNew ;
}

方案5 : 使用Java8特性去重->推荐

1
2
3
4
public static List<String> delRepeat(List<String> list) {
     List<String> myList = list.stream().distinct().collect(Collectors.toList());
	 return myList ;
}

List的复制 (浅拷贝与深拷贝)

开门见山的说,List的复制其实是很常见的,List其本质就是数组,而其存储的形式是地址

./3.png

如图所示,将List A列表复制时,其实相当于A的内容复制给了B,java中相同内容的数组指向同一地址,即进行浅拷贝后A与B指向同一地址。

造成的后果就是,改变B的同时也会改变A,因为改变B就是改变B所指向地址的内容,由于A也指向同一地址,所以A与B一起改变。

这也就是List的浅拷贝,其常见的实现方式有如下几种:

浅拷贝

1、遍历循环复制
1
2
3
4
List<Person> destList=new ArrayList<Person>(srcList.size());  
for(Person p : srcList){  
    destList.add(p);  
}  
2、使用List实现类的构造方法
1
List<Person> destList=new ArrayList<Person>(srcList);  
3、使用list.addAll()方法
1
2
List<Person> destList=new ArrayList<Person>();  
destList.addAll(srcList);  
4、使用System.arraycopy()方法
1
2
3
Person[] srcPersons=srcList.toArray(new Person[0]);  
Person[] destPersons=new Person[srcPersons.length];  
System.arraycopy(srcPersons, 0, destPersons, 0, srcPersons.length);  
测试及结果
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
printList(destList); //打印未改变B之前的A 
srcList.get(0).setAge(100);//改变B  
printList(destList); //打印改变B后的A
//打印结果
123-->20  
ABC-->21  
abc-->22  
123-->100  
ABC-->21  
abc-->22

List 深拷贝

./4.png

如图,深拷贝就是将A复制给B的同时,给B创建新的地址,再将地址A的内容传递到地址B。ListA与ListB内容一致,但是由于所指向的地址不同,所以改变相互不受影响。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.apache.commons.collections.CollectionUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CopyTest {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }

        //list深度拷贝
        List<Integer> newList = new ArrayList<>();
        CollectionUtils.addAll(newList, new Object[list.size()]);
        Collections.copy(newList, list);
        newList.set(0, 10);
        
        System.out.println("原list值:" + list);
        System.out.println("新list值:" + newList);
    }
}

测试结果

1
2
原list值:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
新list值:[10, 1, 2, 3, 4, 5, 6, 7, 8, 9]

小结

Java对对象和基本的数据类型的处理是不一样的。在Java中用对象的作为入口参数的传递则缺省为”引用传递”,也就是说仅仅传递了对象的一个”引用”,这个”引用”的概念同C语言中的指针引用是一样的。当函数体内部对输入变量改变时,实质上就是在对这个对象的直接操作。 除了在函数传值的时候是”引用传递”,在任何用”=”向对象变量赋值的时候都是”引用传递”。

在浅复制的情况下,源数据被修改破坏之后,使用相同引用指向该数据的目标集合中的对应元素也就发生了相同的变化。因此,在需求要求必须深复制的情况下,要是使用上面提到的方法,请确保List中的T类对象是不易被外部修改和破坏的。

对象列表的深度复制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package top.luxd.Test;

import org.apache.commons.collections.CollectionUtils;
import org.junit.Test;
import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CopyTest implements Serializable {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }

        //list深度拷贝
        List<Integer> newList = new ArrayList<>();
        CollectionUtils.addAll(newList, new Object[list.size()]);
        Collections.copy(newList, list);
        newList.set(0, 10);
        System.out.println("原list值:" + list);
        System.out.println("新list值:" + newList);
    }

    @Test
    public void test() throws IOException, ClassNotFoundException {
        UserTest user1 = new UserTest("小明", 18);
        UserTest user2 = new UserTest("小红", 16);
        List<UserTest> list = new ArrayList<>();
        list.add(user1);
        list.add(user2);
        System.out.println("原List:" + list);

        // 进行深度复制
//        List<UserTest> listNew = new ArrayList<>();
//        for (int i = 0; i < list.size(); i += 1) {
//            listNew.add((UserTest) list.get(i).clone());
//        }

        List<UserTest> listNew = deepCopy(list);
        System.out.println("对新list进行操作");
        for (UserTest userTest : listNew) {
            userTest.setAge(99);
        }
        System.out.println("原list" + list);
        System.out.println("新list" + listNew);
    }


    class UserTest implements Serializable {
        String id;
        int age;
        
        public UserTest(String id, int age) {
            this.id = id;
            this.age = age;
        }

        public UserTest() {
        }

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "UserTest{" +
                    "id='" + id + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

    //关键代码 运行序列化和反序列化  进行深度拷贝
    public static <T> List<T> deepCopy(List<T> src) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(byteOut);
        out.writeObject(src);
        ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
        ObjectInputStream in = new ObjectInputStream(byteIn);
        @SuppressWarnings("unchecked")
        List<T> dest = (List<T>) in.readObject();
        return dest;
    }
}

  结果

1
2
3
4
原List:[UserTest{id='小明', age=18}, UserTest{id='小红', age=16}]
对新list进行操作
原list[UserTest{id='小明', age=18}, UserTest{id='小红', age=16}]
新list[UserTest{id='小明', age=99}, UserTest{id='小红', age=99}]

List根据下标修改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//        1 List 是通过指针指向地址来查询和存储数据的。而如果直接将一个List赋值给另一个List。则会导致该两个List都指向同一个地址。而导致如果后一个List的元素值改变,前一个List的元素值也随之改变。这个要记住。
        List list1 = new ArrayList();
        list1.add("1");
        list1.add("2");
        list1.add("3");
        System.out.println("list1:" + list1);

        List list2 = new ArrayList();

        list2 = list1;//简单的直接赋值
        list2.set(1, "12");
        System.out.println("list1:" + list1);
        System.out.println("list2:" + list2);

//        2 所以为了避免出现上述现象和后果。则通过将元素值赋值给list的方法。这样则不会导致上述现象。
        List list3 = new ArrayList();
        list3.add("1");
        list3.add("2");
        list3.add("3");
        System.out.println("list3:" + list3);

        List list4 = new ArrayList();

        for (int i = 0; i < list3.size(); i++) {//通过循环来赋值给另一个List
            Object object = list3.get(i);
            list4.add(object);
        }
        list4.set(1, "12");
        System.out.println("list3:" + list3);
        System.out.println("list4:" + list4);

List转Map的三种方法

一、 ListMap

1
 Map<Long, User> maps = userList.stream().collect(Collectors.toMap(User::getId,Function.identity()));

看来还是使用JDK 1.8方便一些。

二、 另外,转换成Map的时候,可能出现key一样的情况,如果不指定一个覆盖规则,上面的代码是会报错的。转成Map的时候,最好使用下面的方式:

1
 Map<Long, User> maps =  userList.stream().collect(Collectors.toMap(User::getId,  Function.identity(), (key1, key2) -> key2));

三、 有时候,希望得到的Map的值不是对象,而是对象的某个属性,那么可以用下面的方式:

1
Map<Long, String> maps = userList.stream().collect(Collectors.toMap(User::getId, User::getAge, (key1, key2) -> key2));

四、 ListID分组 Map<Integer,List>

1
2
3
4
 Map<Integer, List> groupBy = appleList.stream().collect(Collectors.groupingBy(Apple::getId));

System.err.println(groupBy:+groupBy);
 {1=[Apple{id=1,  name=苹果1, money=3.25, num=10}, Apple{id=1, name=苹果2, money=1.35,  num=20}], 2=[Apple{id=2, name=香蕉, money=2.89, num=30}], 3=[Apple{id=3, name=荔枝, money=9.99, num=40}]}

Java两个List的交集,并集

⽅法⼀:使⽤apache的CollectionUtils⼯具类(推荐)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
publicstaticvoidmain(String[] args){
    String[] arrayA =new String[]{"1","2","3","4"};
    String[] arrayB =new String[]{"3","4","5","6"};
    List<String> listA = Arrays.asList(arrayA);
    List<String> listB = Arrays.asList(arrayB);
    
//1、并集 union
    System.out.println(CollectionUtils.union(listA, listB));
//输出: [1, 2, 3, 4, 5, 6]
    
//2、交集 intersection
    System.out.println(CollectionUtils.intersection(listA, listB));
//输出:[3, 4]

//3、交集的补集(析取)disjunction
    System.out.println(CollectionUtils.disjunction(listA, listB));
//输出:[1, 2, 5, 6]

//4、差集(扣除)
    System.out.println(CollectionUtils.subtract(listA, listB));
//输出:[1, 2]
}

⽅法⼆:List⾃带⽅法

需求 list的方法 说明 备注
交集 listA.retainAll(listB) listA内容变为listA和listB都存在的对象 listB不变
差集 listA.removeAll(listB) listA中存在的listB的内容去重 listB不变
并集 listA.removeAll(listB) listA.addAll(listB) 为了去重,listA先取差集,然后追加全部的listB listB不变
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
publicstaticvoidmain(String[] args){
    String[] arrayA =new String[]{"1","2","3","4"};
    String[] arrayB =new String[]{"3","4","5","6"};
    List<String> listA = Arrays.asList(arrayA);
    List<String> listB = Arrays.asList(arrayB);

//1、交集
    List<String>  jiaoList =new ArrayList<>(listA);
    jiaoList.retainAll(listB);
    System.out.println(jiaoList);
//输出:[3, 4]
    

//2、差集
    List<String>  chaList =new ArrayList<>(listA);
    chaList.removeAll(listB);
    System.out.println(chaList);
//输出:[1, 2]

//3、并集先做差集再做添加所有)
 (
    List<String>  bingList =new ArrayList<>(listA);
    bingList.removeAll(listB);// bingList为[1, 2]

    bingList.addAll(listB);//添加[3,4,5,6]
    System.out.println(bingList);
//输出:[1, 2, 3, 4, 5, 6]
}

注意 : intersection和retainAll的差别

要注意的是它们的返回类型是不⼀样的,intersection返回的是⼀个新的List集合,⽽retainAll返回是Bollean类型那就说明retainAll⽅法是 对原有集合进⾏处理再返回原有集合,会改变原有集合中的内容。

个⼈观点:1、从性能⾓度来考虑的话,List⾃带会⾼点,因为它不⽤再创建新的集合。2、需要注意的是:因为retainAll因为会改变原有集合,所以该集合需要多次使⽤就不适合⽤retainAll。

注意: Arrays.asList将数组转集合不能进⾏add和remove操作。

原因:调⽤Arrays.asList()⽣产的List的add、remove⽅法时报异常,这是由Arrays.asList() 返回的市Arrays的内部类ArrayList, ⽽ 不是java.util.ArrayList。Arrays的内部类ArrayList和java.util.ArrayList都是继承AbstractList,remove、add等⽅法AbstractList中 是默认throw UnsupportedOperationException⽽且不作任何操作。java.util.ArrayList重新了这些⽅法⽽Arrays的内部类ArrayList没 有重新,所以会抛出异常。

所以正确做法如下

1
2
3
4
String[] array ={"1","2","3","4","5"};
List<String> list = Arrays.asList(array);
List arrList =new ArrayList(list);
arrList.add("6");

⽅法三:JDK1.8 stream 新特性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    String[] arrayA =new String[]{"1","2","3","4"};
    String[] arrayB =new String[]{"3","4","5","6"};
    List<String> listA = Arrays.asList(arrayA);
    List<String> listB = Arrays.asList(arrayB);

	// 交集
    List<String> intersection = listA.stream().filter(item -> listB.contains(item)).collect(toList());
    System.out.println(intersection);
	//输出:[3, 4]

	// 差集(list1 - list2)
    List<String> reduceList = listA.stream().filter(item ->!listB.contains(item)).collect(toList());
    System.out.println(reduceList);
	//输出:[1, 2]

	// 并集(新建集合:1、是因为不影响原始集合。2、Arrays.asList不能add和remove操作。
    List<String> listAll = listA.parallelStream().collect(toList());
    List<String> listAll2 = listB.parallelStream().collect(toList());
    listAll.addAll(listAll2);
    System.out.println(listAll);
	//输出:[1, 2, 3, 4, 3, 4, 5, 6]


	// 去重并集
    List<String> list =new ArrayList<>(listA);
    list.addAll(listB);
    List<String> listAllDistinct = list.stream().distinct().collect(toList());
    System.out.println(listAllDistinct);
	//输出:[1, 2, 3, 4, 5, 6]

总结 : 这三种推荐第⼀种⽅式,因为第⼆种还需要确定该集合是否被多次调⽤。第三种可读性不⾼。

对象集合交、并、差处理 因为对象的equels⽐较是⽐较两个对象的内存地址,所以除⾮是同⼀对象,否则equel返回永远是false。

但我们实际开发中 在我们的业务系统中判断对象时有时候需要的不是⼀种严格意义上的相等,⽽是⼀种业务上的对象相等。在这种情况 下,原⽣的equals⽅法就不能满⾜我们的需求了,所以这个时候我们需要重写equals⽅法。

说明 :String为什么可以使⽤equels⽅法为什么只要字符串相等就为true,那是因为String类重写了equal和hashCode⽅法,⽐较的是值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class Person {
    private String name;
    private Integer age;

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    /**
     * 为什么重写equals ⽅法⼀定要重写hashCode⽅法下⾯也会讲
     */
    @Override
    public int hashCode() {
        String result = name + age;
        return result.hashCode();
    }

    /**
     * 重写equals⽅法根据name和age都相同那么对象就默认相同
     */
    @Override
    public boolean equals(Object obj) {
        Person u = (Person) obj;
        return this.getName().equals(u.getName()) && (this.age.equals(u.getAge()));
    }

    /**
     * 重写
     * ⽅法
     * toString
     */
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

    //        这⾥根据name和age都相同那么就默认相同对象。
    public static void main(String[] args) {
        List<Person> personList = Lists.newArrayList();
        Person person1 = new Person("⼩⼩", 3);
        Person person2 = new Person("中中", 4);
        personList.add(person1);
        personList.add(person2);
        List<Person> person1List = Lists.newArrayList();
        Person person3 = new Person("中中", 4);
        Person person4 = new Person("⼤⼤", 5);
        person1List.add(person3);
        person1List.add(person4);
        /**
         1、差集
         */
        System.out.println(CollectionUtils.subtract(personList, person1List));
        //输出:[Person { name = '⼩⼩', age=3}]
        /**
         1、并集
         */
        System.out.println(CollectionUtils.union(personList, person1List));
        //输出:[Person {name = '⼩⼩', age=3}, Person{name='中中', age=4}, Person{name='⼤⼤', age=5}]
        /**
         3、交集
         */
        System.out.println(CollectionUtils.intersection(personList, person1List));
        //输出:[Person {name = '中中', age=4}]
        /**
         4、交集的补集(析取)
         */
        System.out.println(CollectionUtils.disjunction(personList, person1List));
        // 输出:[Person {name = '⼩⼩', age=3}, Person{name='⼤⼤', age=5}]
    }

在ArrayList的循环中删除元素,会不会出现问题?

在 ArrayList 的循环中删除元素,会不会出现问题?我开始觉得应该会有什么问题吧,但是不知道问题会在哪里。在经历了一番测试和查阅之后,发现这个“小”问题并不简单!

不在循环中的删除,是没有问题的,否则这个方法也没有存在的必要了嘛,我们这里讨论的是在循环中的删除,而对 ArrayList 的循环方法也是有多种的,这里定义一个类方法 remove(),先来看段代码吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class ArrayListTest {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();
        list.add("aa");
        list.add("bb");
        list.add("aa");
        list.add("bb");
        list.add("cc");
        // 删除元素 aa
        remove(list, "aa");
        for (String str : list) {
            System.out.println(str);
        }
    }
    public static void remove(ArrayList<String> list, String elem) {
        // 不同的循环及删除方法
        // 方法一:普通for循环正序删除,删除过程中元素向左移动,不能删除重复的元素
//        for (int i = 0; i < list.size(); i++) {
//            if (list.get(i).equals("bb")) {
//                list.remove(list.get(i));
//            }
//        }
        // 方法二:普通for循环倒序删除,删除过程中元素向左移动,可以删除重复的元素
//        for (int i = list.size() - 1; i >= 0; i--) {
//            if (list.get(i).equals("bb")) {
//                list.remove(list.get(i));
//            }
//        }
        // 方法三:增强for循环删除,使用ArrayList的remove()方法删除,产生并发修改异常 ConcurrentModificationException
//        for (String str : list) {
//            if (str.equals("aa")) {
//                list.remove(str);
//            }
//        }

        // 方法四:迭代器,使用ArrayList的remove()方法删除,产生并发修改异常 ConcurrentModificationException
//        Iterator iterator = list.iterator();
//        while (iterator.hasNext()) {
//            if(iterator.next().equals(elem)) {
//                list.remove(iterator.next());
//            }
//        }

        // 方法五:迭代器,使用迭代器的remove()方法删除,可以删除重复的元素,但不推荐
//        Iterator iterator = list.iterator();
//        while (iterator.hasNext()) {
//            if(iterator.next().equals(elem)) {
//                iterator.remove();
//            }
//        }
    }
}

这里我测试了五种不同的删除方法,一种是普通的 for 循环,一种是增强的 foreach 循环,还有一种是使用迭代器循环,一共这三种循环方式。也欢迎你点击文末的 “阅读全文”,留言和我们讨论哦!

上面这几种删除方式呢,在删除 list 中单个的元素,也即是没有重复的元素,如 “cc”。在方法三和方法四中都会产生并发修改异常 ConcurrentModificationException,这两个删除方式中都用到了 ArrayList 中的 remove() 方法(快去上面看看代码吧)。而在删除 list 中重复的元素时,会有这么两种情况,一种是这两个重复元素是紧挨着的,如 “bb”,另一种是这两个重复元素没有紧挨着,如 “aa”。删除这种元素时,方法一在删除重复但不连续的元素时是正常的,但在删除重复且连续的元素时,会出现删除不完全的问题,这种删除方式也是用到了 ArrayList 中的 remove() 方法。而另外两种方法都是可以正常删除的,但是不推荐第五种方式,这个后面再说。

经过对运行结果的分析,发现问题都指向了 ArrayList 中的 remove() 方法,(感觉有种侦探办案的味道,可能是代码写多了的错觉吧,txtx…)那么看 ArrayList 源码是最好的选择了,下面是我截取的关键代码(Java1.8)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

可以看到这个 remove() 方法被重载了,一种是根据下标删除,一种是根据元素删除,这也都很好理解。

根据下标删除的 remove() 方法,大致的步骤如下:

  • 1、检查有没有下标越界,就是检查一下当前的下标有没有大于等于数组的长度
  • 2、列表被修改(add和remove操作)的次数加1
  • 3、保存要删除的值
  • 4、计算移动的元素数量
  • 5、删除位置后面的元素向左移动,这里是用数组拷贝实现的
  • 6、将最后一个位置引用设为 null,使垃圾回收器回收这块内存
  • 7、返回删除元素的值

根据元素删除的 remove() 方法,大致的步骤如下:

  • 1、元素值分为null和非null值
  • 2、循环遍历判等
  • 3、调用 fastRemove(i) 函数
    • 3.1、修改次数加1
    • 3.2、计算移动的元素数量
    • 3.3、数组拷贝实现元素向左移动
    • 3.4、将最后一个位置引用设为 null
    • 3.5、返回 fase
  • 4、返回 true

这里我有个疑问,第一个 remove() 方法中的代码和 fastRemove() 方法中的代码是完全一样的,第一个 remove() 方法完全可以向第二个 remove() 方法一样调用 fastRemove() 方法嘛,这里代码感觉有些冗余,个人理解有限,还请知道的大佬指教。

我们重点关注的是删除过程,学过数据结构的小伙伴可能手写过这样的删除,下面我画个图来让大家更清楚的看到整个删除的过程。以删除 “bb” 为例,当指到下标为 1 的元素时,发现是 “bb”,此处元素应该被删除,根据上面的删除步骤可知,删除位置后面的元素要向前移动,移动之后 “bb” 后面的 “bb” 元素下标为1,后面的元素下标也依次减1,这是在 i = 1 时循环的操作。在下一次循环中 i = 2,第二个 “bb” 元素就被遗漏了,所以这种删除方法在删除连续重复元素时会有问题。

./31.jpeg

循环中的正序删除.jpg

但是如果我们使 i 递减循环,也即是方法二的倒序循环,这个问题就不存在了,如下图。

./32.jpeg

循环中的倒序删除.jpg

既然我们已经搞清不能正常删除的原因,那么再来看看方法五中可以正常删除的原因。方法五中使用的是迭代器中的 remove() 方法,通过阅读 ArrayList 的源码可以发现,有两个私有内部类,Itr 和 ListItr,Itr 实现自 Iterator 接口,ListItr 继承 Itr 类和实现自 ListIterator 接口。Itr 类中也有一个 remove() 方法,迭代器实际调用的也正是这个 remove() 方法,我也截取这个方法的源码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
private class Itr implements Iterator<E>
private class ListItr extends Itr implements ListIterator<E> 
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification(); // 检查修改次数

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

可以看到这个 remove() 方法中调用了 ArrayList 中的 remove() 方法,那为什么方法四会抛出并发修改异常而这里就没有问题呢?这里注意 expectedModCount 变量和 modCount 变量,modCount 在前面的代码中也见到了,它记录了 list 修改的次数,而前面还有一个 expectedModCount,这个变量的初值和 modCount 相等。在 ArrayList.this.remove(lastRet); 代码前面,还调用了检查修改次数的方法 checkForComodification(),这个方法里面做的事情很简单,如果 modCount 和 expectedModCount 不相等,那么就抛出 ConcurrentModificationException,而在这个 remove() 方法中存在 ``expectedModCount = modCount`,两个变量值在 ArrayList 的 remove() 方法后,进行了同步,所以不会有异常抛出,并且在循环过程中,也不会遗漏连续重复的元素,所以可以正常删除。上面这些代码都是在单线程中执行的,如果换到多线程中,方法五不能保证两个变量修改的一致性,结果具有不确定性,所以不推荐这种方法。而方法一在单线程和多线程中都是可以正常删除的,多线程中测试代码如下,这里我只模拟了三个线程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import java.util.ArrayList;
import java.util.Iterator;

public class MultiThreadArrayList {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();
        list.add("aa");
        list.add("bb");
        list.add("bb");
        list.add("aa");
        list.add("cc");
        list.add("dd");
        list.add("dd");
        list.add("cc");
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                remove(list,"aa");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread2 = new Thread() {
            @Override
            public void run() {
                remove(list, "bb");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread3 = new Thread() {
            @Override
            public void run() {
                remove(list, "dd");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        // 使各个线程处于就绪状态
        thread1.start();
        thread2.start();
        thread3.start();
        // 等待前面几个线程完成
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (String str : list) {
            System.out.println(str);
        }
    }

    public static void remove(ArrayList<String> list, String elem) {
        // 普通for循环倒序删除,删除过程中元素向左移动,不影响连续删除
        for (int i = list.size() - 1; i >= 0; i--) {
            if (list.get(i).equals(elem)) {
                list.remove(list.get(i));
            }
        }

        // 迭代器删除,多线程环境下无法使用
//        Iterator iterator = list.iterator();
//        while (iterator.hasNext()) {
//            if(iterator.next().equals(elem)) {
//                iterator.remove();
//            }
//        }
    }
}

既然 Java 的循环删除有问题,发散一下思维,Python 中的列表删除会不会也有这样的问题呢,我抱着好奇试了试,发现下面的方法一也同样存在不能删除连续重复元素的问题,方法二则是报列表下标越界的异常,测试代码如下,这里我只测试了单线程环境:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
list = []
list.append("aa")
list.append("bb")
list.append("bb")
list.append("aa")
list.append("cc")
# 方法一
# for str in list:
#     if str == "bb":
#         list.remove(str)
# 方法二
for i in range(len(list)):
    if list[i] == "bb":
        list.remove(list[i])
for str in list:
    print(str)

总结:一道看似很简单的问题,没想到背后却有这么多的知识,真是感觉自己要学的还很多,遇到方法细节的问题,我觉得直接看源码是最好的解决方法,另外我觉得在后面的版本的 JDK 中,可以增加一个在循环中删除连续元素的方法嘛,不然这里对于没有发现这个问题的人真是个坑。

JAVA:List 与 数组 相互转换

一、 List 转化成 数组

1. list.toArray();

直接将 list 转换成 Object[] 类型的 数组

Object : 对象类,是所有类的父类

1
Object[]  ans1 = list.toArray();

2. list.toArray(T[] a);

输出指定类型的数组,输出的数组类型与括号中参数类型一致;

必须是包装类(String、Integer、Character等),不能是基本数据类型了(string、int、char);

1
2
// 创建数组时: int [ ] arr = new int [ ] {}; 使用的是基本数据类型
Integer[] ans2 = list.toArray(new Integer[list.size()]);

案例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 创建一个list,并且对list赋值        
List<Integer>  list = new ArrayList<>();       
for (int i = 1; i < 11; i++) {
	list.add(i);
}
 
//方法一:
Object[]  ans1 = list.toArray();
System.out.println("1:" + Arrays.toString(ans1));
 
// 方法二:
Integer[] ans2 = list.toArray(new Integer[list.size()]);
System.out.println("2:" + Arrays.toString(ans2));
 
1:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

二、数组 转换成 list

1. Arrays.asList( );

注意该方法的返回值是java.util.Arrays类中一个私有静态内部类java.util.Arrays.ArrayList, 它并非java.util.ArrayList类。 java.util.Arrays.ArrayList类具有set(),get(),contains()等方法, 但是不支持添加add()或删除remove()方法,调用这些方法会报错。

也就是说,此种方法残缺:重新得到的 list 不能 add( ) 或者 remove( );

1
2
3
4
5
6
7
8
// 因为list中是包装类。所以数组创建时也需要使用包装类 
Integer[] num = new Integer[]{1,2,3,4,5,6,7,8,9};
List<Integer> ans1 = Arrays.asList(num);
 
//此时得到的list 不能 使用 add() 与 remove()方法;
// 解决办法:
//    创建一个新的list 对象,将残缺的list加入进去     
List<Integer> list = new ArrayList<>(ans1);

2、Collections.addAll( );(此种方法最实用)

直接创建一个新的 list 对象,然后使用Collections.addAll( ) 方法。

1
2
3
Integer[] num = new Integer[]{1,2,3,4,5,6,7,8,9};
List<Integer> ans2 = new ArrayList<>();
Collections.addAll(ans2,num);

Map

特点

map 没有无序,不能下标访问,只能通过keyvalue进行访问,能增加随机访问的速度

修改

map 直接put进行key,value覆盖修改即可,无需其他操作

遍历Map的几种方法

java中的map遍历有多种方法,从最早的Iterator,到java5支持的foreach,再到java8 Lambda,让我们一起来看下具体的用法以及各自的优缺点

先初始化一个map

1
2
3
public class TestMap {
  public static Map<Integer, Integer> map = new HashMap<Integer, Integer>();
}

keySet values

如果只需要map的key或者value,用map的keySet或values方法无疑是最方便的

返回值类型Set<k> 方法是: keySet() :返回此映射中包含的键的 Set 视图将Map中所有的键存入到Set集合,因为Set具备迭代器,所有迭代方式取出所有的键再根据get()方法 ,获取每一个键对应的值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  // KeySet 获取key
  public void testKeySet() {
    for (Integer key : map.keySet()) {
      System.out.println(key);
    }
  }
  // values 获取value
  public void testValues() {
    for (Integer value : map.values()) {
      System.out.println(value);
    }
  }

keySet get(key)

如果需要同时获取key和value,可以先获取key,然后再通过map的get(key)获取value

需要说明的是,该方法不是最优选择,一般不推荐使用

1
2
3
4
5
6
  // keySet get(key) 获取key and value
  public void testKeySetAndGetKey() {
    for (Integer key : map.keySet()) {
      System.out.println(key + ":" + map.get(key));
    }
  }

entrySet

通过对map entrySet的遍历,也可以同时拿到key和value,一般情况下,性能上要优于上一种,这一种也是最常用的遍历方法

返回值类型:Set<Map.Entry<K,V>>方法是:entrySet()取出的是关系,关系中包含keyvalue,其中Map.Entry<k,V>来表示这种数据类型即:将Map集合中的映射关系存入到Set集合中,这个关系的数据类型为:Map.Entry

Map.Entry接口,此接口在java.util包中,其实Entry也是一个接口,它是Map接口中的一个内部接口 ,getKey()getValue是接口Map.Entry<K,V>中的方法,返回对应的键和对应的值

1
2
3
4
5
6
  // entrySet 获取key and value
  public void testEntry() {
    for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
      System.out.println(entry.getKey() + ":" + entry.getValue());
    }
  }

Iterator

对于上面的几种foreach都可以用Iterator代替,其实foreach在java5中才被支持,foreach的写法看起来更简洁

但Iterator也有其优势:在用foreach遍历map时,如果改变其大小,会报错,但如果只是删除元素,可以使用Iterator的remove方法删除元素

HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。 AbstractMap实现了Map接口。 Map接口里面有一个forEach方法,@since 1.8。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
default void forEach(BiConsumer<? super K, ? super V> action) {
       Objects.requireNonNull(action);
       for (Map.Entry<K, V> entry : entrySet()) {
           K k;
           V v;
           try {
               k = entry.getKey();
               v = entry.getValue();
           } catch(IllegalStateException ise) {
               // this usually means the entry is no longer in the map.
               throw new ConcurrentModificationException(ise);
           }
           action.accept(k, v);
       }
   }

官方解释: 对此映射中的每个条目执行给定操作,直到所有条目已处理或操作引发异常。除非由实现类指定,操作将在入口集迭代的顺序(如果指定了迭代顺序)。 操作引发的异常将中继到调用方。

解读: 使用了try catch 抛出的异常为ConcurrentModificationException,标示在线程并发进行读写的时候会出现异常,即,不支持并发操作。

Map集合中是没有迭代器 的 ,Map集合取 出键值的原理:将map集合转成set集合,再通过迭代器取出

1
2
3
4
5
6
7
8
9
  // Iterator entrySet 获取key and value
  public void testIterator() {
    Iterator<Map.Entry<Integer, Integer>> it = map.entrySet().iterator();
    while (it.hasNext()) {
      Map.Entry<Integer, Integer> entry = it.next();
      System.out.println(entry.getKey() + ":" + entry.getValue());
      // it.remove(); 删除元素
    }
  }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//Map.Entry的剖析和getKey(),getValue()的由来:
interface Map{
 //此内部接口的特点是静态的,而且是对外提供访问的
     public static interface Entry{

         //因为具体情况要视情况而定,所以是抽象的,具体由子类来实现

         public abstract Object getKey();
         public abstract Object getValue();   
                  }
             }
abstract class HashMap implements Map{
    //内部类实现Map.Entry
    abstract class Hahs implements Map.Entry{
        public Object getKey(){}
        public Object getValue(){}
    }
    
}

keySet()取值方式示例图:

./16.png

entrySet()取值方式示例图:

./17.png

注意

Entry是接口 ,但是为什么不定义定义在外面呢?

Entry代表的是映射关系,先有Map集合,才有的映射关系,所以它是Map集合内部的事物,因此将Entry定义为 在Map的内部集合,可以直接访问Map集合中的元素 在API文档中有 public static interface Map.Entry<K,V> 其中被static静态修饰,只有接口在 成员位置上才能加静态修饰符 ,说明其是内部接口

Lambda

java8提供了Lambda表达式支持,语法看起来更简洁,可以同时拿到key和value,不过,经测试,性能低于entrySet,所以更推荐用entrySet的方式

1
2
3
4
5
6
  // Lambda 获取key and value
  public void testLambda() {
    map.forEach((key, value) -> {
      System.out.println(key + ":" + value);
    });
  }

简单性能测试

用10万条数据,做了一个简单性能测试,数据类型为Integer,map实现选取HashMap

1
2
3
4
5
  static {
    for (int i = 0; i < 100000; i++) {
      map.put(i, 1);
    }
  }

测试结果如下:

1
2
3
4
5
6
KeySet           392
Values           320
keySet get(key)  552
entrySet         465
entrySet Iterator508
Lambda           536

需要说明的是,map存储的数据类型,map的大小,以及map的不同实现方式都会影响遍历的性能,所以该测试结果仅供参考

总结

如果只是获取key,或者value,推荐使用keySet或者values方式

如果同时需要key和value推荐使用entrySet

如果需要在遍历过程中删除元素推荐使用Iterator

如果需要在遍历过程中增加元素,可以新建一个临时map存放新增的元素,等遍历完毕,再把临时map放到原来的map中

分析

一般来讲使用entrySet的方式进行遍历是效率最高的,因为hashMap内部的存储结构就是基于Entry的数组,在用这种方式进行遍历时,只需要遍历一次即可。而使用其他方式的时间复杂度可以会提高,例如:keySet方式,每次都需要通过key值去计算对应的hash,然后再通过hash获取对应的结果值,因此效率较低。

扩容机制

  1. 导读 上期分享了HashMap的key定位以及数据节点的设计, 本期就下面三个问题来分享下个人对于HashMap扩容的理解: .1 HashMap为什么要扩容? 何时扩容? .2 负载因子为什么是0.75? .3 HashMap如何扩容;

  2. HashMap为什么要扩容 经过上期分享, 我们都知道HashMap在构建初始是可以指定table(hash槽)的长度的, 假设我们设定了2, 这时候有10万数据要插入, 最好的情况就是两边各是5万, 最差的情况就是一边是10万, 显然这个时候hash冲突已经很严重了, 为了解决冲突, 我们就需要对table进行扩容, 所以HashMap的扩容就是加长table的长度, 来减少hash冲突的概率;

  3. HashMap何时扩容 HashMap是如何来判定何时该扩容的呢?

    1
    2
    3
    4
    5
    6
    7
    8
    
         final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
            if ((tab = table) == null || (n = tab.length) == 0)
                    n = (tab = resize()).length;
            ...
            if (++size > threshold)
                resize();
            ...
        }
    

上面代码是HashMap::put的核心实现, 我将与本问题无关的代码都省略了, HashMap会在两个地方进行resize(扩容): .1 HashMap实行了懒加载, 新建HashMap时不会对table进行赋值, 而是到第一次插入时, 进行resize时构建table; .2 当HashMap.size 大于 threshold时, 会进行resize;threshold的值我们在上一次分享中提到过: 当第一次构建时, 如果没有指定HashMap.table的初始长度, 就用默认值16, 否则就是指定的值; 然后不管是第一次构建还是后续扩容, threshold = table.length * loadFactor;

  1. 为什么是0.75 HashMap的扩容时取决于threshold, 而threshold取决于loadFactor, loadFactor(负载因子)HashMap的默认值是0.75(3/4), 那么为什么当HashMap的容量超过3/4时就需要扩容了呢? 为什么不是1/2扩容 或者 等于table.length时扩容呢? 答案就在HashMap的注释中:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
 /**
     *  Ideally, under random hashCodes, the frequency of
     *     nodes in bins follows a Poisson distribution
     *  (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     *     parameter of about 0.5 on average for the default resizing
     *     threshold of 0.75, although with a large variance because of
     *     resizing granularity. Ignoring variance, the expected
     *     occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     *     factorial(k)). The first values are:
     *
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * 
     */

根据统计学的结果, hash冲突是符合泊松分布的, 而冲突概率最小的是在7-8之间, 都小于百万分之一了; 所以HashMap.loadFactor选取只要在7-8之间的任意值即可, 但是为什么就选了3/4这个值, 我们看了HashMap的扩容机制也就知道了;

  1. HashMap如何扩容 因为扩容的代码比较长, 我用文字来叙述下HashMap扩容的过程: .1 如果table == null, 则为HashMap的初始化, 生成空table返回即可; .2 如果table不为空, 需要重新计算table的长度, newLength = oldLength « 1(注, 如果原oldLength已经到了上限, 则newLength = oldLength); .3 遍历oldTable: .3.2 首节点为空, 本次循环结束; .3.1 无后续节点, 重新计算hash位, 本次循环结束; .3.2 当前是红黑树, 走红黑树的重定位; .3.3 当前是链表, JAVA7时还需要重新计算hash位, 但是JAVA8做了优化, 通过(e.hash & oldCap) == 0来判断是否需要移位; 如果为真则在原位不动, 否则则需要移动到当前hash槽位 + oldCap的位置;

./11.png

HashMap::resize的核心就是上图, 链表与红黑树的resize过程大同小异: 红黑树是把构建新链表的过程变为构建两颗新的红黑树, 定位table中的index都是用的 e.hash & oldCap == 0 来判断; 再来看下 e.hash & oldCap == 0为什么可以判断当前节点是否需要移位, 而不是再次计算hash; 仍然是原始长度为16举例:

1
2
3
4
5
6
7
8
9
   old:
   10: 0000 1010
   15: 0000 1111
    &: 0000 1010    
    
   new:
   10: 0000 1010
   31: 0001 1111
    &: 0001 1010    

从上面的示例可以很轻易的看出, 两次indexFor()的差别只是第二次参与位于比第一次左边有一位从0变为1, 而这个变化的1刚好是oldCap, 那么只需要判断原key的hash这个位上是否为1: 若是1, 则需要移动至oldCap + i的槽位, 若为0, 则不需要移动; 这也是HashMap的长度必须保证是2的倍数的原因, 正因为这种环环相扣的设计, HashMap.loadFactor的选值是3/4就能理解了, table.length * 3/4可以被优化为(table.length >> 2) << 2) - (table.length >> 2) == table.length - (table.lenght >> 2), JAVA的位运算比乘除的效率更高, 所以取3/4在保证hash冲突小的情况下兼顾了效率;

  1. JDK8对JDK7的优化

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
        void transfer(Entry[] newTable, boolean rehash) {
            int newCapacity = newTable.length;
            for (Entry<K,V> e : table) {
                while(null != e) {
                    Entry<K,V> next = e.next;
                    if (rehash) {
                        e.hash = null == e.key ? 0 : hash(e.key);
                    }
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                }
            }
        }
    

./12.png

上面的代码是JAVA7中对于HashMap节点重新定位的代码, 我们都知道HashMap是非线程安全的, 最主要的原因是他在resize的时候会形成环形链表, 然后导致get时死循环; resize前的HashMap如下图所示:

这时候有两个线程需要插入第四个节点, 这个时候HashMap就需要做resize了,我们先假设线程已经resize完成, 而线程二必须等线程一完成再resize:

./13.png

经过线程一resize后, 可以发现a b节点的顺序被反转了, 这时候我们来看线程二:

./14.png

.1 线程二的开始点是只获取到A节点, 还没获取他的next; .2 这时候线程一resize完成, a.next = null; b.next = a; newTable[i] = b; .3 线程二开始执行, 获取A节点的next节点, a.next = null; .4 接着执行 a.next = newTable[i]; 因为这时候newTable[i]已经是B节点了, 并且b.next = a; 那么我们把newTablei赋值给a.next后, 就会线程a-b-a这样的环形链表了, 也就是上图的结果; .5 因为第三步的a.next已经是null, 所以C节点就丢失了; .6 那这时候来查位于1节点的数据D(其实不存在), 因为 d != a, 会接着查a.next, 也就是b; 但是b != d, 所以接着查b.next, 但是b.next还是a; 这就悲剧了, 在循环里出不去了; 这就是JDK7resize最大的缺陷, 会形成死循环; 那么JDK8做了优化以后, 死循环的问题解除了吗?

./15.png

通过上图我们发现JDK8的resize是让节点的顺序发生改变的, 也就是没有倒排问题了;也是假设有两个线程, 线程一已执行完成, 这时候线程二来执行: .1 因为顺序没变, 所以node1.next还是node2, 只是node2.next从node3变成了null; .2 而且JDK8是在遍历完所有节点之后, 才对形成的两个链表进行关联table的, 所以不会像JAVA7一般形成A-B-A问题了; .3 但是如果并发了, JAVA的HashMap还是没有解决丢数据的问题, 但是不和JAVA7一般有数据倒排以及死循环的问题了;

HashMap设计时就是没有保证线程安全的, 所以在多线程环境请使用ConcurrentHashMap;

原文

以下为JDK8 resize方法详解

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table; //当前所有元素所在的数组,称为老的元素数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length; //老的元素数组长度
        int oldThr = threshold;	// 老的扩容阀值设置
        int newCap, newThr = 0;	// 新数组的容量,新数组的扩容阀值都初始化为0
        if (oldCap > 0) {	// 如果老数组长度大于0,说明已经存在元素
            // PS1
            if (oldCap >= MAXIMUM_CAPACITY) { // 如果数组元素个数大于等于限定的最大容量(2的30次方)
                // 扩容阀值设置为int最大值(2的31次方 -1 ),因为oldCap再乘2就溢出了。
                threshold = Integer.MAX_VALUE;	
                return oldTab;	// 返回老的元素数组
            }
 
           /*
            * 如果数组元素个数在正常范围内,那么新的数组容量为老的数组容量的2倍(左移1位相当于乘以2)
            * 如果扩容之后的新容量小于最大容量  并且  老的数组容量大于等于默认初始化容量(16),那么新数组的扩容阀值设置为老阀值的2倍。(老的数组容量大于16意味着:要么构造函数指定了一个大于16的初始化容量值,要么已经经历过了至少一次扩容)
            */
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
 
        // PS2
        // 运行到这个else if  说明老数组没有任何元素
        // 如果老数组的扩容阀值大于0,那么设置新数组的容量为该阀值
        // 这一步也就意味着构造该map的时候,指定了初始化容量。
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            // 能运行到这里的话,说明是调用无参构造函数创建的该map,并且第一次添加元素
            newCap = DEFAULT_INITIAL_CAPACITY;	// 设置新数组容量 为 16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 设置新数组扩容阀值为 16*0.75 = 12。0.75为负载因子(当元素个数达到容量了4分之3,那么扩容)
        }
 
        // 如果扩容阀值为0 (PS2的情况)
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);  // 参见:PS2
        }
        threshold = newThr; // 设置map的扩容阀值为 新的阀值
        @SuppressWarnings({"rawtypes","unchecked"})
            // 创建新的数组(对于第一次添加元素,那么这个数组就是第一个数组;对于存在oldTab的时候,那么这个数组就是要需要扩容到的新数组)
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;	// 将该map的table属性指向到该新数组
        if (oldTab != null) {	// 如果老数组不为空,说明是扩容操作,那么涉及到元素的转移操作
            for (int j = 0; j < oldCap; ++j) { // 遍历老数组
                Node<K,V> e;
                if ((e = oldTab[j]) != null) { // 如果当前位置元素不为空,那么需要转移该元素到新数组
                    oldTab[j] = null; // 释放掉老数组对于要转移走的元素的引用(主要为了使得数组可被回收)
                    if (e.next == null) // 如果元素没有有下一个节点,说明该元素不存在hash冲突
                        // PS3
                        // 把元素存储到新的数组中,存储到数组的哪个位置需要根据hash值和数组长度来进行取模
                        // 【hash值  %   数组长度】   =    【  hash值   & (数组长度-1)】
                        //  这种与运算求模的方式要求  数组长度必须是2的N次方,但是可以通过构造函数随意指定初始化容量呀,如果指定了17,15这种,岂不是出问题了就?没关系,最终会通过tableSizeFor方法将用户指定的转化为大于其并且最相近的2的N次方。 15 -> 16、17-> 32
                        newTab[e.hash & (newCap - 1)] = e;
 
                        // 如果该元素有下一个节点,那么说明该位置上存在一个链表了(hash相同的多个元素以链表的方式存储到了老数组的这个位置上了)
                        // 例如:数组长度为16,那么hash值为1(1%16=1)的和hash值为17(17%16=1)的两个元素都是会存储在数组的第2个位置上(对应数组下标为1),当数组扩容为32(1%32=1)时,hash值为1的还应该存储在新数组的第二个位置上,但是hash值为17(17%32=17)的就应该存储在新数组的第18个位置上了。
                        // 所以,数组扩容后,所有元素都需要重新计算在新数组中的位置。
 
 
                    else if (e instanceof TreeNode)  // 如果该节点为TreeNode类型
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);  // 此处单独展开讨论
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;  // 按命名来翻译的话,应该叫低位首尾节点
                        Node<K,V> hiHead = null, hiTail = null;  // 按命名来翻译的话,应该叫高位首尾节点
                        // 以上的低位指的是新数组的 0  到 oldCap-1 、高位指定的是oldCap 到 newCap - 1
                        Node<K,V> next;
                        // 遍历链表
                        do {  
                            next = e.next;
                            // 这一步判断好狠,拿元素的hash值  和  老数组的长度  做与运算
                            // PS3里曾说到,数组的长度一定是2的N次方(例如16),如果hash值和该长度做与运算,结果为0,就说明该hash值小于数组长度(例如hash值为7),
                            // 那么该hash值再和新数组的长度取摸的话mod值也不会放生变化,所以该元素的在新数组的位置和在老数组的位置是相同的,所以该元素可以放置在低位链表中。
                            if ((e.hash & oldCap) == 0) {  
                                // PS4
                                if (loTail == null) // 如果没有尾,说明链表为空
                                    loHead = e; // 链表为空时,头节点指向该元素
                                else
                                    loTail.next = e; // 如果有尾,那么链表不为空,把该元素挂到链表的最后。
                                loTail = e; // 把尾节点设置为当前元素
                            }
 
                            // 如果与运算结果不为0,说明hash值大于老数组长度(例如hash值为17)
                            // 此时该元素应该放置到新数组的高位位置上
                            // 例:老数组长度16,那么新数组长度为32,hash为17的应该放置在数组的第17个位置上,也就是下标为16,那么下标为16已经属于高位了,低位是[0-15],高位是[16-31]
                            else {  // 以下逻辑同PS4
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) { // 低位的元素组成的链表还是放置在原来的位置
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {  // 高位的元素组成的链表放置的位置只是在原有位置上偏移了老数组的长度个位置。
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead; // 例:hash为 17 在老数组放置在0下标,在新数组放置在16下标;    hash为 18 在老数组放置在1下标,在新数组放置在17下标;                   
                        }
                    }
                }
            }
        }
        return newTab; // 返回新数组
    }

原文

HashMap 的 7 种遍历方式与性能分析

首先,给大家说声抱歉~

事情经过是这样子的,五一节前我发布了一篇文章《HashMap 的 7 种遍历方式与性能分析!》,但是好心的网友却发现了一个问题,他说 测试时使用了 sout 打印信息会导致测试的结果不准确,因为这样测试的话,大部分的性能消耗其实来源于信息打印,我细想了一下,说的确实有道理,于是我就重写了测试部分的代码

但是不写不知道,一写吓一跳,删除了打印信息的代码之后,惊奇的发现,之前得出的“EntrySet 和 KeySet 性能相近”的结论是错误的,并且我也把 JMH 测试吞吐量的代码换成了平均执行时间,因为这样看起来更直观。而测试的结果是,EntrySet 和 KeySet 的性能相差在一倍以上,详见下文。

本文改动了两处内容:

  • 去掉了测试代码中的打印信息,把测试类型从吞吐量测试改成了平均执行时间测试(这样看起来更直观);
  • 新增了 EntrySet 和 KeySet 性能差别的原因分析。

备注:以上内容为更正后的完整文章。

随着 JDK 1.8 Streams API 的发布,使得 HashMap 拥有了更多的遍历的方式,但应该选择那种遍历方式?反而成了一个问题。

本文先从 HashMap 的遍历方法讲起,然后再从性能、原理以及安全性等方面,来分析 HashMap 各种遍历方式的优势与不足,本文主要内容如下图所示:

./20.png

HashMap 遍历

HashMap 遍历从大的方向来说,可分为以下 4 类

  1. 迭代器(Iterator)方式遍历;
  2. For Each 方式遍历;
  3. Lambda 表达式遍历(JDK 1.8+);
  4. Streams API 遍历(JDK 1.8+)。

但每种类型下又有不同的实现方式,因此具体的遍历方式又可以分为以下 7 种:

  1. 使用迭代器(Iterator)EntrySet 的方式进行遍历;
  2. 使用迭代器(Iterator)KeySet 的方式进行遍历;
  3. 使用 For Each EntrySet 的方式进行遍历;
  4. 使用 For Each KeySet 的方式进行遍历;
  5. 使用 Lambda 表达式的方式进行遍历;
  6. 使用 Streams API 单线程的方式进行遍历;
  7. 使用 Streams API 多线程的方式进行遍历。

接下来我们来看每种遍历方式的具体实现代码。

1.迭代器 EntrySet
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Integer, String> entry = iterator.next();
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }
    }
}

以上程序的执行结果为:

1

Java

2

JDK

3

Spring Framework

4

MyBatis framework

5

Java中文社群

2.迭代器 KeySet
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        Iterator<Integer> iterator = map.keySet().iterator();
        while (iterator.hasNext()) {
            Integer key = iterator.next();
            System.out.println(key);
            System.out.println(map.get(key));
        }
    }
}

以上程序的执行结果为:

1

Java

2

JDK

3

Spring Framework

4

MyBatis framework

5

Java中文社群

3.ForEach EntrySet
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        for (Map.Entry<Integer, String> entry : map.entrySet()) {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }
    }
}

以上程序的执行结果为:

1

Java

2

JDK

3

Spring Framework

4

MyBatis framework

5

Java中文社群

4.ForEach KeySet
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        for (Integer key : map.keySet()) {
            System.out.println(key);
            System.out.println(map.get(key));
        }
    }
}

以上程序的执行结果为:

1

Java

2

JDK

3

Spring Framework

4

MyBatis framework

5

Java中文社群

5.Lambda
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        map.forEach((key, value) -> {
            System.out.println(key);
            System.out.println(value);
        });
    }
}

以上程序的执行结果为:

1

Java

2

JDK

3

Spring Framework

4

MyBatis framework

5

Java中文社群

6.Streams API 单线程
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        map.entrySet().stream().forEach((entry) -> {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        });
    }
}

以上程序的执行结果为:

1

Java

2

JDK

3

Spring Framework

4

MyBatis framework

5

Java中文社群

7.Streams API 多线程
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        map.entrySet().parallelStream().forEach((entry) -> {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        });
    }
}

以上程序的执行结果为:

4

MyBatis framework

5

Java中文社群

1

Java

2

JDK

3

Spring Framework

性能测试

接下来我们使用 Oracle 官方提供的性能测试工具 JMH(Java Microbenchmark Harness,JAVA 微基准测试套件)来测试一下这 7 种循环的性能。

首先,我们先要引入 JMH 框架,在 pom.xml 文件中添加如下配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.23</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.23</version>
    <scope>provided</scope>
</dependency>

然后编写测试代码,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 1s
@Fork(1) // fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class HashMapCycleTest {
    static Map<Integer, String> map = new HashMap() {{
        // 添加数据
        for (int i = 0; i < 100; i++) {
            put(i, "val:" + i);
        }
    }};

    public static void main(String[] args) throws RunnerException {
        // 启动基准测试
        Options opt = new OptionsBuilder()
                .include(HashMapCycle.class.getSimpleName()) // 要导入的测试类
                .output("/Users/admin/Desktop/jmh-map.log") // 输出测试结果的文件
                .build();
        new Runner(opt).run(); // 执行测试
    }

    @Benchmark
    public void entrySet() {
        // 遍历
        Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Integer, String> entry = iterator.next();
            Integer k = entry.getKey();
            String v = entry.getValue();
        }
    }

    @Benchmark
    public void forEachEntrySet() {
        // 遍历
        for (Map.Entry<Integer, String> entry : map.entrySet()) {
            Integer k = entry.getKey();
            String v = entry.getValue();
        }
    }

    @Benchmark
    public void keySet() {
        // 遍历
        Iterator<Integer> iterator = map.keySet().iterator();
        while (iterator.hasNext()) {
            Integer k = iterator.next();
            String v = map.get(k);
        }
    }

    @Benchmark
    public void forEachKeySet() {
        // 遍历
        for (Integer key : map.keySet()) {
            Integer k = key;
            String v = map.get(k);
        }
    }

    @Benchmark
    public void lambda() {
        // 遍历
        map.forEach((key, value) -> {
            Integer k = key;
            String v = value;
        });
    }

    @Benchmark
    public void streamApi() {
        // 单线程遍历
        map.entrySet().stream().forEach((entry) -> {
            Integer k = entry.getKey();
            String v = entry.getValue();
        });
    }

    public void parallelStreamApi() {
        // 多线程遍历
        map.entrySet().parallelStream().forEach((entry) -> {
            Integer k = entry.getKey();
            String v = entry.getValue();
        });
    }
}

所有被添加了 @Benchmark 注解的方法都会被测试,因为 parallelStream 为多线程版本性能一定是最好的,所以就不参与测试了,其他 6 个方法的测试结果如下:

./21.png

其中 Units 为 ns/op 意思是执行完成时间(单位为纳秒),而 Score 列为平均执行时间, ± 符号表示误差。从以上结果可以看出,两个 entrySet 的性能相近,并且执行速度最快,接下来是 stream ,然后是两个 keySet,性能最差的是 KeySet

注:以上结果基于测试环境:JDK 1.8 / Mac mini (2018) / Idea 2020.1

结论

从以上结果可以看出 entrySet 的性能比 keySet 的性能高出了一倍之多,因此我们应该尽量使用 entrySet 来实现 Map 集合的遍历

字节码分析

要理解以上的测试结果,我们需要把所有遍历代码通过 javac 编译成字节码来看具体的原因。

编译后,我们使用 Idea 打开字节码,内容如下:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.example;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

public class HashMapTest {
    static Map<Integer, String> map = new HashMap() {
        {
            for(int var1 = 0; var1 < 2; ++var1) {
                this.put(var1, "val:" + var1);
            }

        }
    };

    public HashMapTest() {
    }

    public static void main(String[] var0) {
        entrySet();
        keySet();
        forEachEntrySet();
        forEachKeySet();
        lambda();
        streamApi();
        parallelStreamApi();
    }

    public static void entrySet() {
        Iterator var0 = map.entrySet().iterator();

        while(var0.hasNext()) {
            Entry var1 = (Entry)var0.next();
            System.out.println(var1.getKey());
            System.out.println((String)var1.getValue());
        }

    }

    public static void keySet() {
        Iterator var0 = map.keySet().iterator();

        while(var0.hasNext()) {
            Integer var1 = (Integer)var0.next();
            System.out.println(var1);
            System.out.println((String)map.get(var1));
        }

    }

    public static void forEachEntrySet() {
        Iterator var0 = map.entrySet().iterator();

        while(var0.hasNext()) {
            Entry var1 = (Entry)var0.next();
            System.out.println(var1.getKey());
            System.out.println((String)var1.getValue());
        }

    }

    public static void forEachKeySet() {
        Iterator var0 = map.keySet().iterator();

        while(var0.hasNext()) {
            Integer var1 = (Integer)var0.next();
            System.out.println(var1);
            System.out.println((String)map.get(var1));
        }

    }

    public static void lambda() {
        map.forEach((var0, var1) -> {
            System.out.println(var0);
            System.out.println(var1);
        });
    }

    public static void streamApi() {
        map.entrySet().stream().forEach((var0) -> {
            System.out.println(var0.getKey());
            System.out.println((String)var0.getValue());
        });
    }

    public static void parallelStreamApi() {
        map.entrySet().parallelStream().forEach((var0) -> {
            System.out.println(var0.getKey());
            System.out.println((String)var0.getValue());
        });
    }
}

从结果可以看出,除了 Lambda 和 Streams API 之外,通过迭代器循环和 for 循环的遍历的 EntrySet 最终生成的代码是一样的,他们都是在循环中创建了一个遍历对象 Entry ,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public static void entrySet() {
    Iterator var0 = map.entrySet().iterator();
    while(var0.hasNext()) {
        Entry var1 = (Entry)var0.next();
        System.out.println(var1.getKey());
        System.out.println((String)var1.getValue());
    }
}
public static void forEachEntrySet() {
    Iterator var0 = map.entrySet().iterator();
    while(var0.hasNext()) {
        Entry var1 = (Entry)var0.next();
        System.out.println(var1.getKey());
        System.out.println((String)var1.getValue());
    }
}

KeySet 的代码也是类似的,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public static void keySet() {
    Iterator var0 = map.keySet().iterator();
    while(var0.hasNext()) {
        Integer var1 = (Integer)var0.next();
        System.out.println(var1);
        System.out.println((String)map.get(var1));
    }
} 
public static void forEachKeySet() {
    Iterator var0 = map.keySet().iterator();
    while(var0.hasNext()) {
        Integer var1 = (Integer)var0.next();
        System.out.println(var1);
        System.out.println((String)map.get(var1));
    }
}

所以我们在使用迭代器或是 for 循环 EntrySet 时,他们的性能都是相同的,因为他们最终生成的字节码基本都是一样的;同理 KeySet 的两种遍历方式也是类似的。

性能分析

EntrySet 之所以比 KeySet 的性能高是因为,KeySet 在循环时使用了 map.get(key),而 map.get(key) 相当于又遍历了一遍 Map 集合去查询 key 所对应的值。为什么要用“又”这个词?那是因为在使用迭代器或者 for 循环时,其实已经遍历了一遍 Map 集合了,因此再使用 map.get(key) 查询时,相当于遍历了两遍

EntrySet 只遍历了一遍 Map 集合,之后通过代码“Entry<Integer, String> entry = iterator.next()”把对象的 keyvalue 值都放入到了 Entry 对象中,因此再获取 keyvalue 值时就无需再遍历 Map 集合,只需要从 Entry 对象中取值就可以了。

所以,EntrySet 的性能比 KeySet 的性能高出了一倍,因为 KeySet 相当于循环了两遍 Map 集合,而 EntrySet 只循环了一遍

安全性测试

从上面的性能测试结果和原理分析,我想大家应该选用那种遍历方式,已经心中有数的,而接下来我们就从「安全」的角度入手,来分析那种遍历方式更安全。

我们把以上遍历划分为四类进行测试:迭代器方式、For 循环方式、Lambda 方式和 Stream 方式,测试代码如下。

1.迭代器方式
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<Integer, String> entry = iterator.next();
    if (entry.getKey() == 1) {
        // 删除
        System.out.println("del:" + entry.getKey());
        iterator.remove();
    } else {
        System.out.println("show:" + entry.getKey());
    }
}

以上程序的执行结果:

show:0

del:1

show:2

测试结果:迭代器中循环删除数据安全

2.For 循环方式
1
2
3
4
5
6
7
8
9
for (Map.Entry<Integer, String> entry : map.entrySet()) {
    if (entry.getKey() == 1) {
        // 删除
        System.out.println("del:" + entry.getKey());
        map.remove(entry.getKey());
    } else {
        System.out.println("show:" + entry.getKey());
    }
}

以上程序的执行结果:

./22.pngimage.png

测试结果:For 循环中删除数据非安全

3.Lambda 方式
1
2
3
4
5
6
7
8
map.forEach((key, value) -> {
    if (key == 1) {
        System.out.println("del:" + key);
        map.remove(key);
    } else {
        System.out.println("show:" + key);
    }
});

以上程序的执行结果:

./23.png测试结果:Lambda 循环中删除数据非安全

Lambda 删除的正确方式

1
2
3
4
5
// 根据 map 中的 key 去判断删除
map.keySet().removeIf(key -> key == 1);
map.forEach((key, value) -> {
    System.out.println("show:" + key);
});

以上程序的执行结果:

show:0

show:2

从上面的代码可以看出,可以先使用 LambdaremoveIf 删除多余的数据,再进行循环是一种正确操作集合的方式。

4.Stream 方式
1
2
3
4
5
6
7
8
map.entrySet().stream().forEach((entry) -> {
    if (entry.getKey() == 1) {
        System.out.println("del:" + entry.getKey());
        map.remove(entry.getKey());
    } else {
        System.out.println("show:" + entry.getKey());
    }
});

以上程序的执行结果:

./24.png

测试结果:Stream 循环中删除数据非安全

Stream 循环的正确方式

1
2
3
4
5
6
7
map.entrySet().stream().filter(m -> 1 != m.getKey()).forEach((entry) -> {
    if (entry.getKey() == 1) {
        System.out.println("del:" + entry.getKey());
    } else {
        System.out.println("show:" + entry.getKey());
    }
});

以上程序的执行结果:

show:0

show:2

从上面的代码可以看出,可以使用 Stream 中的 filter 过滤掉无用的数据,再进行遍历也是一种安全的操作集合的方式。

小结

我们不能在遍历中使用集合 map.remove() 来删除数据,这是非安全的操作方式,但我们可以使用迭代器的 iterator.remove() 的方法来删除数据,这是安全的删除集合的方式。同样的我们也可以使用 Lambda 中的 removeIf 来提前删除数据,或者是使用 Stream 中的 filter 过滤掉要删除的数据进行循环,这样都是安全的,当然我们也可以在 for 循环前删除数据在遍历也是线程安全的。

总结

本文我们讲了 HashMap 4 种遍历方式:迭代器、for、lambda、stream,以及具体的 7 种遍历方法,综合性能和安全性来看,我们应该尽量使用迭代器(Iterator)来遍历 EntrySet 的遍历方式来操作 Map 集合,这样就会既安全又高效了。

Java HashMap的死循环

在淘宝内网里看到同事发了贴说了一个CPU被100%的线上故障,并且这个事发生了很多次,原因是在Java语言在并发情况下使用HashMap造成Race Condition,从而导致死循环。这个事情我4、5年前也经历过,本来觉得没什么好写的,因为Java的HashMap是非线程安全的,所以在并发下必然出现问题。但是,我发现近几年,很多人都经历过这个事(在网上查“HashMap Infinite Loop”可以看到很多人都在说这个事)所以,觉得这个是个普遍问题,需要写篇疫苗文章说一下这个事,并且给大家看看一个完美的“Race Condition”是怎么形成的。

问题的症状

从前我们的Java代码因为一些原因使用了HashMap这个东西,但是当时的程序是单线程的,一切都没有问题。后来,我们的程序性能有问题,所以需要变成多线程的,于是,变成多线程后到了线上,发现程序经常占了100%的CPU,查看堆栈,你会发现程序都Hang在了HashMap.get()这个方法上了,重启程序后问题消失。但是过段时间又会来。而且,这个问题在测试环境里可能很难重现。

我们简单的看一下我们自己的代码,我们就知道HashMap被多个线程操作。而Java的文档说HashMap是非线程安全的,应该用ConcurrentHashMap。

但是在这里我们可以来研究一下原因。

Hash表数据结构

我需要简单地说一下HashMap这个经典的数据结构。

HashMap通常会用一个指针数组(假设为table[])来做分散所有的key,当一个key被加入时,会通过Hash算法通过key算出这个数组的下标i,然后就把这个<key, value>插到table[i]中,如果有两个不同的key被算在了同一个i,那么就叫冲突,又叫碰撞,这样会在table[i]上形成一个链表。

我们知道,如果table[]的尺寸很小,比如只有2个,如果要放进10个keys的话,那么碰撞非常频繁,于是一个O(1)的查找算法,就变成了链表遍历,性能变成了O(n),这是Hash表的缺陷(可参看《Hash Collision DoS 问题》)。

所以,Hash表的尺寸和容量非常的重要。一般来说,Hash表这个容器当有数据要插入时,都会检查容量有没有超过设定的thredhold,如果超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的无素都需要被重算一遍。这叫rehash,这个成本相当的大。

相信大家对这个基础知识已经很熟悉了。

HashMap的rehash源代码

下面,我们来看一下Java的HashMap的源代码。

Put一个Key,Value对到Hash表中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public V put(K key, V value)
{
    ......
    //算Hash值
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    //如果该key已被插入,则替换掉旧的value (链接操作)
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    //该key不存在,需要增加一个结点
    addEntry(hash, key, value, i);
    return null;
}

检查容量是否超标

1
2
3
4
5
6
7
8
void addEntry(int hash, K key, V value, int bucketIndex)
{
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    //查看当前的size是否超过了我们设定的阈值threshold,如果超过,需要resize
    if (size++ >= threshold)
        resize(2 * table.length);
} 

新建一个更大尺寸的hash表,然后把数据从老的Hash表中迁移到新的Hash表中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void resize(int newCapacity)
{
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    ......
    //创建一个新的Hash Table
    Entry[] newTable = new Entry[newCapacity];
    //将Old Hash Table上的数据迁移到New Hash Table上
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}

迁移的源代码,注意高亮处:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void transfer(Entry[] newTable)
{
    Entry[] src = table;
    int newCapacity = newTable.length;
    //下面这段代码的意思是:
    //  从OldTable里摘一个元素出来,然后放到NewTable中
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
} 

好了,这个代码算是比较正常的。而且没有什么问题。

正常的ReHash的过程

画了个图做了个演示。

  • 我假设了我们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。

  • 最上面的是old hash 表,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以后都冲突在table[1]这里了。

  • 接下来的三个步骤是Hash表 resize成4,然后所有的<key,value> 重新rehash的过程

./26.jpg

并发下的Rehash

**1)假设我们有两个线程。**我用红色和浅蓝色标注了一下。

我们再回头看一下我们的 transfer代码中的这个细节:

1
2
3
4
5
6
7
do {
    Entry<K,V> next = e.next; // <--假设线程一执行到这里就被调度挂起了
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
} while (e != null);

而我们的线程二执行完成了。于是我们有下面的这个样子。

./27.jpg

注意,因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转后。

2)线程一被调度回来执行。

  • 先是执行 newTalbe[i] = e;
  • 然后是e = next,导致了e指向了key(7),
  • 而下一次循环的next = e.next导致了next指向了key(3)

./28.jpg

3)一切安好。

线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移

./29.jpg

4)环形链接出现。

e.next = newTable[i] 导致 key(3).next 指向了 key(7)

注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。

./30.jpg

于是,当我们的线程一调用到,HashTable.get(11)时,悲剧就出现了——Infinite Loop。

设计思想

局部性原理,当时设计HashMap的大叔采用头插法而没有采用尾插法有一点考虑是性能优化,认为最近put进去的元素,被get的概率相对较其他元素大,采用头插法能够更快得获取到最近插入的元素。

但头插法的设计有一个特点,就是扩容之后,链表上的元素顺序会反过来,这也是死循环的一个重要原因。

问题答疑

1、有同学认为线程B对链表的操作,线程A怎么会看到呢?不是有线程可见性问题吗?

首先得理解线程可见性的原因是因为有cpu缓存,在线程执行之前,读取了操作数,在操作过程中操作数都在CPU缓存中,在线程没有将操作数写入主存之前,线程中对操作数的修改则对于其他线程是不可见的。

而在hashMap扩容的过程中,线程操作的是堆中的对象,线程持有的是对对象的引用。引用就是一个地址,对引用的修改就是对堆中对象的修改。线程B的操作对于线程A的操作是透明的,所以线程A能看到线程B对链表的修改。

其它

有人把这个问题报给了Sun,不过Sun不认为这个是一个问题。因为HashMap本来就不支持并发。要并发就用ConcurrentHashmap

http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6423457

我在这里把这个事情记录下来,只是为了让大家了解并体会一下并发环境下的危险。

修复

Java1.8版本将头插法修复为尾插法,解决了问题,但HashMap本身是线程不安全的(不管哪个jdk版本),所以请不要在并发访问的场景下直接使用HashMap。如果在并发访问的场景下,建议采用concurrentHashMap。

Java 集合 –tableSizeFor

在看 HashMap 源码的时候有这么一段代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
private static final int MAXIMUM_CAPACITY = 1 << 30;

private static final int tableSizeFor(int c) {
    int n = c - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

第一眼看上去完全看不懂,这几个右移按位或是什么意思

运行一个例子看看

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static final int MAXIMUM_CAPACITY = 1 << 30;

private static int tableSizeFor(int c) {
    int n = c - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

public static void main(String[] args) {
    System.out.println(tableSizeFor(6));
    System.out.println(tableSizeFor(7));
    System.out.println(tableSizeFor(10));
    System.out.println(tableSizeFor(15));
    System.out.println(tableSizeFor(18));
}

// 输出
8
8
16
16
32

输入 6,7 都是输出 8

输入 10, 15 输出 16

输入 18 输出 32

输出的都是 2 的指数幂,其实这个方法是用于找到大于等于输入参数的的最小的 2 的指数幂。为什么需要这样的方法,因为 hashmap 的容量大小都是 2 的指数幂。

以输入 22 为例子, n = c - 1, n 为 21

./25.webp

每一次右移之后与上一次的结果做按位或操作(只要有一个位是 1,结果就是 1),通过几次操作之后将原本二进制最高位为 1 的后面几位全部至 1,最后再加 1,得到一个 2 的指数幂。

至于为什么一开始要执行 n = c - 1; 这是为了防止 c 已经是 2 的幂,如果 c 已经是 2 的幂, 又没有执行这个减 1 操作,则执行完后面的几条无符号右移操作之后,返回的结果将是这个 c 的 2 倍。

Java-Collectors常用的20个方法

返回List集合: toList()

用于将元素累积到List集合中。它将创建一个新List集合(不会更改当前集合)。

1
2
3
List<Integer> integers = Arrays.asList(1,2,3,4,5,6,6);
integers.stream().map(x -> x*x).collect(Collectors.toList());
// output: [1,4,9,16,25,36,36]
返回Set集合: toSet()

用于将元素累积到Set集合中。它会删除重复元素。

1
2
3
List<Integer> integers = Arrays.asList(1,2,3,4,5,6,6);
integers.stream().map(x -> x*x).collect(Collectors.toSet());
// output: [1,4,9,16,25,36]
返回指定的集合: toCollection()

可以将元素雷击到指定的集合中。

1
2
3
4
5
6
List<Integer> integers = Arrays.asList(1,2,3,4,5,6,6);
integers
    .stream()
    .filter(x -> x >2)
    .collect(Collectors.toCollection(LinkedList::new));
// output: [3,4,5,6,6]
计算元素数量: Counting()

用于返回计算集合中存在的元素个数。

1
2
3
4
5
6
List<Integer> integers = Arrays.asList(1,2,3,4,5,6,6);
Long collect = integers
                   .stream()
                   .filter(x -> x <4)
                   .collect(Collectors.counting());
// output: 3
求最小值: minBy()

用于返回列表中存在的最小值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
List<Integer> integers = Arrays.asList(1,2,3,4,5,6,6);
List<String> strings = Arrays.asList("alpha","beta","gamma");
integers
    .stream()
    .collect(Collectors.minBy(Comparator.naturalOrder()))
    .get();
// output: 1
strings
   .stream()
   .collect(Collectors.minBy(Comparator.naturalOrder()))
   .get();
// output: alpha

按照整数排序返回1,按照字符串排序返回alpha

可以使用reverseOrder()方法反转顺序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
List<Integer> integers = Arrays.asList(1,2,3,4,5,6,6);
List<String> strings = Arrays.asList("alpha","beta","gamma");
integers
    .stream()
    .collect(Collectors.minBy(Comparator.reverseOrder()))
    .get();
// output: 6
strings
   .stream()
   .collect(Collectors.minBy(Comparator.reverseOrder()))
   .get();
// output: gamma

同时可以自定义的对象定制比较器。

求最大值: maxBy()

和最小值方法类似,使用maxBy()方法来获得最大值。

1
2
3
4
5
6
List<String> strings = Arrays.asList("alpha","beta","gamma");
strings
   .stream()
   .collect(Collectors.maxBy(Comparator.naturalOrder()))
   .get();
// output: gamma
分区列表:partitioningBy()

用于将一个集合划分为2个集合并将其添加到映射中,1个满足给定条件,另一个不满足,例如从集合中分离奇数。因此它将在map中生成2条数据,1个以truekey,奇数为值,第2个以falsekey,以偶数为值。

1
2
3
4
5
List<String> strings = Arrays.asList("a","alpha","beta","gamma");
Map<Boolean, List<String>> collect1 = strings
          .stream()
          .collect(Collectors.partitioningBy(x -> x.length() > 2));
// output: {false=[a], true=[alpha, beta, gamma]}

这里我们将长度大于2的字符串与其余字符串分开。

返回不可修改的List集合:toUnmodifiableList()

用于创建只读List集合。任何试图对此不可修改List集合进行更改的尝试都将导致UnsupportedOperationException

1
2
3
4
5
List<String> strings = Arrays.asList("alpha","beta","gamma");
List<String> collect2 = strings
       .stream()
       .collect(Collectors.toUnmodifiableList());
// output: ["alpha","beta","gamma"]
返回不可修改的Set集合:toUnmodifiableSet()

用于创建只读Set集合。任何试图对此不可修改Set集合进行更改的尝试都将导致UnsupportedOperationException。它会删除重复元素。

1
2
3
4
5
6
List<String> strings = Arrays.asList("alpha","beta","gamma","alpha");
Set<String> readOnlySet = strings
       .stream()
       .sorted()
       .collect(Collectors.toUnmodifiableSet());
// output: ["alpha","beta","gamma"]
连接元素:Joining()

用指定的字符串链接集合内的元素。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
List<String> strings = Arrays.asList("alpha","beta","gamma");
String collect3 = strings
     .stream()
     .distinct()
     .collect(Collectors.joining(","));
// output: alpha,beta,gamma
String collect4 = strings
     .stream()
     .map(s -> s.toString())
     .collect(Collectors.joining(",","[","]"));
// output: [alpha,beta,gamma]
Long类型集合的平均值:averagingLong()

查找Long类型集合的平均值。

注意:返回的是Double类型而不是 Long类型

1
2
3
4
5
List<Long> longValues = Arrays.asList(100l,200l,300l);
Double d1 = longValues
    .stream()
    .collect(Collectors.averagingLong(x -> x * 2));
// output: 400.0
Integer类型集合的平均值:averagingInt()

查找Integer类型集合的平均值。

注意:返回的是Double类型而不是int 类型

1
2
3
4
5
List<Long> longValues = Arrays.asList(100l,200l,300l);
Double d1 = longValues
    .stream()
    .collect(Collectors.averagingLong(x -> x * 2));
// output: 400.0
Double类型集合的平均值:averagingDouble()

查找Double类型集合的平均值。

1
2
3
4
5
List<Double> doubles = Arrays.asList(1.1,2.0,3.0,4.0,5.0,5.0);
Double d3 = doubles
    .stream()
    .collect(Collectors.averagingDouble(x -> x));
// output: 3.35
创建Map:toMap()

根据集合的值创建Map

1
2
3
4
5
6
List<String> strings = Arrays.asList("alpha","beta","gamma");
Map<String,Integer> map = strings
       .stream()
       .collect(Collectors
          .toMap(Function.identity(),String::length));
// output: {alpha=5, beta=4, gamma=5}
在创建Map时处理列表的重复项

集合中可以包含重复的值,因此,如果想从列表中创建一个Map,并希望使用集合值作为Mapkey,那么需要解析重复的key。由于Map只包含唯一的key,可以使用比较器来实现这一点。

1
2
3
4
5
6
List<String> strings = Arrays.asList("alpha","beta","gamma","beta");
Map<String,Integer> map = strings
        .stream()
        .collect(Collectors
          .toMap(Function.identity(),String::length,(i1,i2) -> i1));
// output: {alpha=5, gamma=5, beta=4}

Function.identity()指向集合中的值,i1i2是重复键的值。可以只保留一个值,这里选择i1,也可以用这两个值来计算任何东西,比如把它们相加,比较和选择较大的那个,等等。

整数求和:summingInt ()

查找集合中所有整数的和。它并不总是初始集合的和,就像我们在下面的例子中使用的我们使用的是字符串列表,首先我们把每个字符串转换成一个等于它的长度的整数,然后把所有的长度相加。

1
2
3
4
5
List<String> strings = Arrays.asList("alpha","beta","gamma");
Integer collect4 = strings
      .stream()
      .collect(Collectors.summingInt(String::length));
// output: 18

或直接集合值和

1
2
3
4
5
List<Integer> integers = Arrays.asList(1,2,3,4,5,6,6);
Integer sum = integers
    .stream()
    .collect(Collectors.summingInt(x -> x));
// output: 27
double求和:summingDouble ()

类似于整数求和,只是它用于双精度值

1
2
3
4
5
List<Double>  doubleValues = Arrays.asList(1.1,2.0,3.0,4.0,5.0,5.0);
Double sum = doubleValues
     .stream()
     .collect(Collectors.summingDouble(x ->x));
// output: 20.1
Long求和:summingLong ()

与前两个相同,用于添加long值或int 值。可以对int 值使用summinglong() ,但不能对long值使用summinglong()

1
2
3
4
5
List<Long> longValues = Arrays.asList(100l,200l,300l);
Long sum = longValues
    .stream()
    .collect(Collectors.summingLong(x ->x));
// output: 600
汇总整数:summarizingInt ()

它给出集合中出现的值的所有主要算术运算值,如所有值的平均值、最小值、最大值、所有值的计数和总和。

1
2
3
4
5
List<Integer> integers = Arrays.asList(1,2,3,4,5,6,6);
IntSummaryStatistics stats = integers
          .stream()
          .collect(Collectors.summarizingInt(x -> x ));
//output: IntSummaryStatistics{count=7, sum=27, min=1, average=3.857143, max=6}

可以使用get方法提取不同的值,如:

1
2
3
4
5
stats.getAverage();   // 3.857143
stats.getMax();       // 6
stats.getMin();       // 1
stats.getCount();     // 7
stats.getSum();       // 27
分组函数:GroupingBy ()

GroupingBy()是一种高级方法,用于从任何其他集合创建Map

1
2
3
4
5
List<String> strings = Arrays.asList("alpha","beta","gamma");
Map<Integer, List<String>> collect = strings
          .stream()
          .collect(Collectors.groupingBy(String::length));
// output: {4=[beta, beta], 5=[alpha, gamma]}

它将字符串长度作为key,并将该长度的字符串列表作为value

1
2
3
4
5
6
List<String> strings = Arrays.asList("alpha","beta","gamma");
Map<Integer, LinkedList<String>> collect1 = strings
            .stream()
            .collect(Collectors.groupingBy(String::length, 
                Collectors.toCollection(LinkedList::new)));
// output: {4=[beta, beta], 5=[alpha, gamma]}

这里指定了Map中需要的列表类型(Libkedlist)。

Java8-Stream集合操作

Stream简介

  • Java 8引入了全新的Stream API。这里的Stream和I/O流不同,它更像具有Iterable的集合类,但行为和集合类又有所不同。
  • stream是对集合对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作,或者大批量数据操作。
  • 只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。

为什么要使用Stream

  • 函数式编程带来的好处尤为明显。这种代码更多地表达了业务逻辑的意图,而不是它的实现机制。易读的代码也易于维护、更可靠、更不容易出错。
  • 高端

实例数据源

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class Data {
    private static List<PersonModel> list = null;

    static {
        PersonModel wu = new PersonModel("wu qi", 18, "男");
        PersonModel zhang = new PersonModel("zhang san", 19, "男");
        PersonModel wang = new PersonModel("wang si", 20, "女");
        PersonModel zhao = new PersonModel("zhao wu", 20, "男");
        PersonModel chen = new PersonModel("chen liu", 21, "男");
        list = Arrays.asList(wu, zhang, wang, zhao, chen);
    }

    public static List<PersonModel> getData() {
        return list;
    }
} 

Filter

  • 遍历数据并检查其中的元素时使用。
  • filter接受一个函数作为参数,该函数用Lambda表达式表示。

./5.webp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
    /**
     * 过滤所有的男性
     */
    public static void fiterSex(){
        List<PersonModel> data = Data.getData();

        //old
        List<PersonModel> temp=new ArrayList<>();
        for (PersonModel person:data) {
            if ("男".equals(person.getSex())){
                temp.add(person);
            }
        }
        System.out.println(temp);
        //new
        List<PersonModel> collect = data
                .stream()
                .filter(person -> "男".equals(person.getSex()))
                .collect(toList());
        System.out.println(collect);
    }

    /**
     * 过滤所有的男性 并且小于20岁
     */
    public static void fiterSexAndAge(){
        List<PersonModel> data = Data.getData();

        //old
        List<PersonModel> temp=new ArrayList<>();
        for (PersonModel person:data) {
            if ("男".equals(person.getSex())&&person.getAge()<20){
                temp.add(person);
            }
        }

        //new 1
        List<PersonModel> collect = data
                .stream()
                .filter(person -> {
                    if ("男".equals(person.getSex())&&person.getAge()<20){
                        return true;
                    }
                    return false;
                })
                .collect(toList());
        //new 2
        List<PersonModel> collect1 = data
                .stream()
                .filter(person -> ("男".equals(person.getSex())&&person.getAge()<20))
                .collect(toList());

    }

Map

  • map生成的是个一对一映射,for的作用
  • 比较常用
  • 而且很简单

./6.webp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
   /**
     * 取出所有的用户名字
     */
    public static void getUserNameList(){
        List<PersonModel> data = Data.getData();

        //old
        List<String> list=new ArrayList<>();
        for (PersonModel persion:data) {
            list.add(persion.getName());
        }
        System.out.println(list);

        //new 1
        List<String> collect = data.stream().map(person -> person.getName()).collect(toList());
        System.out.println(collect);

        //new 2
        List<String> collect1 = data.stream().map(PersonModel::getName).collect(toList());
        System.out.println(collect1);

        //new 3
        List<String> collect2 = data.stream().map(person -> {
            System.out.println(person.getName());
            return person.getName();
        }).collect(toList());
    }

FlatMap

  • 顾名思义,跟map差不多,更深层次的操作
  • 但还是有区别的
  • map和flat返回值不同
  • Map 每个输入元素,都按照规则转换成为另外一个元素。 还有一些场景,是一对多映射关系的,这时需要 flatMap。
  • Map一对一
  • Flatmap一对多
  • map和flatMap的方法声明是不一样的
    • Stream map(Function mapper);
    • Stream flatMap(Function> mapper);
  • map和flatMap的区别:我个人认为,flatMap的可以处理更深层次的数据,入参为多个list,结果可以返回为一个list,而map是一对一的,入参是多个list,结果返回必须是多个list。通俗的说,如果入参都是对象,那么flatMap可以操作对象里面的对象,而map只能操作第一层。

./7.webp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public static void flatMapString() {
        List<PersonModel> data = Data.getData();
        //返回类型不一样
        List<String> collect = data.stream()
                .flatMap(person -> Arrays.stream(person.getName().split(" "))).collect(toList());

        List<Stream<String>> collect1 = data.stream()
                .map(person -> Arrays.stream(person.getName().split(" "))).collect(toList());

        //用map实现
        List<String> collect2 = data.stream()
                .map(person -> person.getName().split(" "))
                .flatMap(Arrays::stream).collect(toList());
        //另一种方式
        List<String> collect3 = data.stream()
                .map(person -> person.getName().split(" "))
                .flatMap(str -> Arrays.asList(str).stream()).collect(toList());
    }

Reduce

  • 感觉类似递归
  • 数字(字符串)累加
  • 个人没咋用过

./8.webp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 public static void reduceTest(){
        //累加,初始化值是 10
        Integer reduce = Stream.of(1, 2, 3, 4)
                .reduce(10, (count, item) ->{
            System.out.println("count:"+count);
            System.out.println("item:"+item);
            return count + item;
        } );
        System.out.println(reduce);

        Integer reduce1 = Stream.of(1, 2, 3, 4)
                .reduce(0, (x, y) -> x + y);
        System.out.println(reduce1);

        String reduce2 = Stream.of("1", "2", "3")
                .reduce("0", (x, y) -> (x + "," + y));
        System.out.println(reduce2);
    }

Collect

  • collect在流中生成列表,map,等常用的数据结构
  • toList()
  • toSet()
  • toMap()
  • 自定义
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
  * toList
  */
public static void toListTest(){
    List<PersonModel> data = Data.getData();
    List<String> collect = data.stream()
        .map(PersonModel::getName)
        .collect(Collectors.toList());
}

/**
  * toSet
  */
public static void toSetTest(){
    List<PersonModel> data = Data.getData();
    Set<String> collect = data.stream()
        .map(PersonModel::getName)
        .collect(Collectors.toSet());
}

/**
  * toMap
  */
public static void toMapTest(){
    List<PersonModel> data = Data.getData();
    Map<String, Integer> collect = data.stream()
        .collect(
        Collectors.toMap(PersonModel::getName, PersonModel::getAge)
    );

    data.stream()
        .collect(Collectors.toMap(per->per.getName(), value->{
            return value+"1";
        }));
}

/**
  * toMap处理冲突
  */
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
 
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
 
@Getter
@Setter
@AllArgsConstructor
public class Person {
    private Integer id;
    private String name;
 
    public static void main(String[] args) {
        List<Person> list = new ArrayList();
        list.add(new Person(1, "1"));
//        list.add(new Person(1, "4"));
        list.add(new Person(2, "2"));
        list.add(new Person(3, "3"));
 
        Map<Integer, Person> collect = list.stream().collect(Collectors.toMap(Person::getId, Function.identity()));
        Map<Integer, Person> collect1 = list.stream().collect(Collectors.toMap(Person::getId, Function.identity(), (a,b)->a));
        Map<Integer, Person> collect2 = list.stream().collect(Collectors.toMap(Person::getId, v -> v, (a,b)->a));
        Collection<Person> values = list.stream().collect(Collectors.toMap(Person::getId, Function.identity(), (a, b) -> a)).values();
        long count = list.stream().collect(Collectors.toMap(Person::getId, Function.identity(), (a, b) -> a)).values().stream().count();
        System.out.println(collect);
        System.out.println(collect1);
        System.out.println(collect2);
        System.out.println(values);
        System.out.println(count);
    }
}

./33.png

使用toMap()函数之后,返回的就是一个Map了,自然会需要key和value。 toMap()的第一个参数就是用来生成key值的,第二个参数就是用来生成value值的。 第三个参数用在key值冲突的情况下:如果新元素产生的key在Map中已经出现过了,第三个参数就会定义解决的办法。

在.collect(Collectors.toMap(Person::getId, v -> v, (a,b)->a))中:

第一个参数:Person:getId表示选择Person的getId作为map的key值;

第二个参数:v->v表示选择将原来的对象作为Map的value值

第三个参数:(a,b)->a中,如果a与b的key值相同,选择a作为那个key所对应的value值。

如果key冲突,不加(a,b)->a会报如下错误

./34.png

./35.png

参考1

参考2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 /**
  * 指定类型
  */
public static void toTreeSetTest(){
    List<PersonModel> data = Data.getData();
    TreeSet<PersonModel> collect = data.stream()
        .collect(Collectors.toCollection(TreeSet::new));
    System.out.println(collect);
}

/**
  * 分组
  */
public static void toGroupTest(){
    List<PersonModel> data = Data.getData();
    Map<Boolean, List<PersonModel>> collect = data.stream()
        .collect(Collectors.groupingBy(per -> "男".equals(per.getSex())));
    System.out.println(collect);
}

/**
  * 分隔
  */
public static void toJoiningTest(){
    List<PersonModel> data = Data.getData();
    String collect = data.stream()
        .map(personModel -> personModel.getName())
        .collect(Collectors.joining(",", "{", "}"));
    System.out.println(collect);
}

/**
  * 自定义
  */
public static void reduce(){
    List<String> collect = Stream.of("1", "2", "3").collect(
        Collectors.reducing(new ArrayList<String>(), x -> Arrays.asList(x), (y, z) -> {
            y.addAll(z);
            return y;
        }));
    System.out.println(collect);
}

Optional

  • Optional 是为核心类库新设计的一个数据类型,用来替换 null 值。
  • 人们对原有的 null 值有很多抱怨,甚至连发明这一概念的Tony Hoare也是如此,他曾说这是自己的一个“价值连城的错误”
  • 用处很广,不光在lambda中,哪都能用
  • Optional.of(T),T为非空,否则初始化报错
  • Optional.ofNullable(T),T为任意,可以为空
  • isPresent(),相当于 !=null
  • ifPresent(T), T可以是一段lambda表达式 ,或者其他代码,非空则执行
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public static void main(String[] args) {


        PersonModel personModel=new PersonModel();

        //对象为空则打出 -
        Optional<Object> o = Optional.of(personModel);
        System.out.println(o.isPresent()?o.get():"-");

        //名称为空则打出 -
        Optional<String> name = Optional.ofNullable(personModel.getName());
        System.out.println(name.isPresent()?name.get():"-");

        //如果不为空,则打出xxx
        Optional.ofNullable("test").ifPresent(na->{
            System.out.println(na+"ifPresent");
        });

        //如果空,则返回指定字符串
        System.out.println(Optional.ofNullable(null).orElse("-"));
        System.out.println(Optional.ofNullable("1").orElse("-"));

        //如果空,则返回 指定方法,或者代码
        System.out.println(Optional.ofNullable(null).orElseGet(()->{
            return "hahah";
        }));
        System.out.println(Optional.ofNullable("1").orElseGet(()->{
            return "hahah";
        }));

        //如果空,则可以抛出异常
        System.out.println(Optional.ofNullable("1").orElseThrow(()->{
            throw new RuntimeException("ss");
        }));


//        Objects.requireNonNull(null,"is null");


        //利用 Optional 进行多级判断
        EarthModel earthModel1 = new EarthModel();
        //old
        if (earthModel1!=null){
            if (earthModel1.getTea()!=null){
                //...
            }
        }
        //new
        Optional.ofNullable(earthModel1)
                .map(EarthModel::getTea)
                .map(TeaModel::getType)
                .isPresent();


//        Optional<EarthModel> earthModel = Optional.ofNullable(new EarthModel());
//        Optional<List<PersonModel>> personModels = earthModel.map(EarthModel::getPersonModels);
//        Optional<Stream<String>> stringStream = personModels.map(per -> per.stream().map(PersonModel::getName));


        //判断对象中的list
        Optional.ofNullable(new EarthModel())
                .map(EarthModel::getPersonModels)
                .map(pers->pers
                        .stream()
                        .map(PersonModel::getName)
                        .collect(toList()))
                .ifPresent(per-> System.out.println(per));


        List<PersonModel> models=Data.getData();
        Optional.ofNullable(models)
                .map(per -> per
                        .stream()
                        .map(PersonModel::getName)
                        .collect(toList()))
                .ifPresent(per-> System.out.println(per));

    }

Java8用Optional处理 null

注意开头增加

1
import java.util.*;
ofNullable

Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。

Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。

Optional 类的引入很好的解决空指针异常。

1
2
3
public static String getGender(Student student) {
	return Optional.ofNullable(student).map(u -> u.getGender()).orElse("Unkown");    
}
ifPresent()

isPresent()方法用于判断包装对象的值是否非空。

ifPresent()方法接受一个Consumer对象(消费函数),如果包装对象的值非空,运行Consumer对象的accept()方法。

1
2
3
public static void printName(Student student){
	Optional.ofNullable(student).ifPresent(u ->  System.out.println("The student name is : " + u.getName()));
}
filter()

filter()方法接受参数为Predicate对象,用于对Optional对象进行过滤,如果符合Predicate的条件,返回Optional对象本身,否则返回一个空的Optional对象。

1
2
3
public static void filterAge(Student student){
	Optional.ofNullable(student).filter( u -> u.getAge() > 18).ifPresent(u ->  System.out.println("The student age is more than 18."));
}
map()

map()方法的参数为Function(函数式接口)对象,map()方法将Optional中的包装对象用Function函数进行运算,并包装成新的Optional对象(包装对象的类型可能改变)。

1
2
3
public static Optional<Integer> getAge(Student student){
	return Optional.ofNullable(student).map(u -> u.getAge()); 
}
flatMap()

跟map()方法不同的是,入参Function函数的返回值类型为Optional类型,而不是U类型,这样flatMap()能将一个二维的Optional对象映射成一个一维的对象。

1
2
3
public static Optional<Integer> getAge(Student student){
	return Optional.ofNullable(student).flatMap(u -> Optional.ofNullable(u.getAge())); 
}
orElse()

orElse()方法功能比较简单,即如果包装对象值非空,返回包装对象值,否则返回入参other的值(默认值)。

1
2
3
public static String getGender(Student student){
	return Optional.ofNullable(student).map(u -> u.getGender()).orElse("Unkown");
}
orElseGet()

orElseGet()方法与orElse()方法类似,区别在于orElseGet()方法的入参为一个Supplier对象,用Supplier对象的get()方法的返回值作为默认值。

orElseGet 可以处理需要根据null对象进行处理的场景。

1
2
3
public static String getGender(Student student){
	return Optional.ofNullable(student).map(u -> u.getGender()).orElseGet(() -> "Unkown");      
}
orElseThrow()

orElseThrow()方法其实与orElseGet()方法非常相似了,入参都是Supplier对象,只不过orElseThrow()的Supplier对象必须返回一个Throwable异常,并在orElseThrow()中将异常抛出:

orElseThrow()方法适用于包装对象值为空时需要抛出特定异常的场景。

1
2
3
public static String getGender1(Student student){
    return Optional.ofNullable(student).map(u -> u.getGender()).orElseThrow(() -> new RuntimeException("Unkown"));      
}

并发

  • stream替换成parallelStream或 parallel
  • 输入流的大小并不是决定并行化是否会带来速度提升的唯一因素,性能还会受到编写代码的方式和核的数量的影响
  • 影响性能的五要素是:数据大小、源数据结构、值是否装箱、可用的CPU核数量,以及处理每个元素所花的时间
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
 //根据数字的大小,有不同的结果
    private static int size=10000000;
    public static void main(String[] args) {
        System.out.println("-----------List-----------");
        testList();
        System.out.println("-----------Set-----------");
        testSet();
    }

    /**
     * 测试list
     */
    public static void testList(){
        List<Integer> list = new ArrayList<>(size);
        for (Integer i = 0; i < size; i++) {
            list.add(new Integer(i));
        }

        List<Integer> temp1 = new ArrayList<>(size);
        //老的
        long start=System.currentTimeMillis();
        for (Integer i: list) {
            temp1.add(i);
        }
        System.out.println(+System.currentTimeMillis()-start);

        //同步
        long start1=System.currentTimeMillis();
        list.stream().collect(Collectors.toList());
        System.out.println(System.currentTimeMillis()-start1);

        //并发
        long start2=System.currentTimeMillis();
        list.parallelStream().collect(Collectors.toList());
        System.out.println(System.currentTimeMillis()-start2);
    }

    /**
     * 测试set
     */
    public static void testSet(){
        List<Integer> list = new ArrayList<>(size);
        for (Integer i = 0; i < size; i++) {
            list.add(new Integer(i));
        }

        Set<Integer> temp1 = new HashSet<>(size);
        //老的
        long start=System.currentTimeMillis();
        for (Integer i: list) {
            temp1.add(i);
        }
        System.out.println(+System.currentTimeMillis()-start);

        //同步
        long start1=System.currentTimeMillis();
        list.stream().collect(Collectors.toSet());
        System.out.println(System.currentTimeMillis()-start1);

        //并发
        long start2=System.currentTimeMillis();
        list.parallelStream().collect(Collectors.toSet());
        System.out.println(System.currentTimeMillis()-start2);
    }

调试

  • list.map.fiter.map.xx 为链式调用,最终调用collect(xx)返回结果
  • 分惰性求值和及早求值
  • 判断一个操作是惰性求值还是及早求值很简单:只需看它的返回值。如果返回值是 Stream,那么是惰性求值;如果返回值是另一个值或为空,那么就是及早求值。使用这些操作的理想方式就是形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果。
  • 通过peek可以查看每个值,同时能继续操作流
1
2
3
4
5
6
7
8
private static void peekTest() {
        List<PersonModel> data = Data.getData();

        //peek打印出遍历的每个per
        data.stream().map(per->per.getName()).peek(p->{
            System.out.println(p);
        }).collect(toList());
    }

Java8 stream().map().collect()用法

集合

List<User> users = getList(); //从数据库查询的用户集合

现在想获取User的身份证号码;在后续的逻辑处理中要用;

常用的方法我们大家都知道,用for循环,

1
2
3
4
List<String> idcards=new ArrayList<String>();//定义一个集合来装身份证号码
for(int i=0;i<users.size();i++){
  idcards.add(users.get(i).getIdcard());
}

这种方法要写好几行代码,有没有简单点的,有,java8 API能一行搞定:

1
List<String> idcards= users.stream().map(User::getIdcard).collect(Collectors.toList())

解释下一这行代码:

  • users:一个实体类的集合,类型为List<User>
  • User:实体类
  • getIdcard:实体类中的get方法,为获取User的idcard

stream()优点

无存储。stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。 为函数式编程而生。对stream的任何修改都不会修改背后的数据源,比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream。 惰式执行。stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。 可消费性。stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。 stream().map()方法的使用示例:

其他例子

再看几个例子:数组字母小写变大写

1
2
3
4
List<String> list= Arrays.asList("a", "b", "c", "d");

List<String> collect =list.stream().map(String::toUpperCase).collect(Collectors.toList());
System.out.println(collect); //[A, B, C, D]

数组所有元素,按某种规律计算:

1
2
3
List<Integer> num = Arrays.asList(1,2,3,4,5);
List<Integer> collect1 = num.stream().map(n -> n * 2).collect(Collectors.toList());
System.out.println(collect1); //[2, 4, 6, 8, 10]

为什么在Java 8中String.chars()是一个整数流?

正如其他人已经提到的那样,这背后的设计决策是为了防止方法和类的爆炸.

不过,我个人认为这是一个非常糟糕的决定,并且鉴于他们不想制作CharStream,这应该是合理的,不同的方法而不是chars(),我会想到:

  • Stream<Character> chars(),这给出了一个盒子字符流,这将有一些轻微的性能损失.
  • IntStream unboxedChars(),将用于性能代码.

但是,我认为这个答案应该专注于展示使用Java 8获得的API的方法,而不是关注为什么它以这种方式完成.

在Java 7中,我会这样做:

1
2
3
for (int i = 0; i < hello.length(); i++) {
    System.out.println(hello.charAt(i));
}

我认为在Java 8中使用它的合理方法如下:

1
2
3
hello.chars()
        .mapToObj(i -> (char)i)
        .forEach(System.out::println);

在这里,我获得了一个IntStream并通过lambda i -> (char)i将它映射到一个对象,这将自动将其打包成一个Stream<Character>,然后我们可以做我们想要的,并仍然使用方法引用作为加号.

请注意,你必须这样做mapToObj,如果你忘记并使用map,那么没有什么会抱怨,但你仍然IntStream会得到一个,你可能会想知道为什么它打印整数值而不是代表字符的字符串.

Java 8的其他丑陋替代品:

通过保留IntStream并希望最终打印它们,您不能再使用方法引用进行打印:

1
2
hello.chars()
        .forEach(i -> System.out.println((char)i));

而且,使用方法引用自己的方法不再起作用了!考虑以下:

1
2
3
private void print(char c) {
    System.out.println(c);
}

然后

1
2
hello.chars()
        .forEach(this::print);

这将产生编译错误,因为可能存在有损转换.

结论:

API是这样设计的,因为不想添加CharStream,我个人认为该方法应该返回一个Stream<Character>,并且当前的解决方法是使用mapToObj(i -> (char)i)on IntStream来能够正常使用它们.

补充

任何API的设计都是一系列的权衡.在Java中,其中一个难题是处理很久以前制定的设计决策.

从1.0开始,基元就一直在Java中.它们使Java成为一种"不纯的"面向对象语言,因为原语不是对象.我相信,添加原语是一种务实的决定,以牺牲面向对象的纯度为代价来提高性能.

这是近20年后我们仍然生活在今天的权衡.Java 5中添加的自动装箱功能大多消除了使用装箱和拆箱方法调用来混乱源代码的需要,但开销仍然存在.在许多情况下,它并不明显.但是,如果您要在内部循环中执行装箱或拆箱,您会发现它可能会产生大量的CPU和垃圾收集开销.

在设计Streams API时,很明显我们必须支持原语.装箱/拆箱开销会破坏并行性带来的任何性能优势.但是,我们不想支持所有原语,因为这会给API增加大量的混乱.(你能真正看到一个用途ShortStream?)“全部"或"无"是设计的舒适场所,但都不可接受.所以我们必须找到合理的"一些"价值.我们结束了与原始的专长int,longdouble.(我个人可能会漏掉int但那只是我.)

因为CharSequence.chars()我们考虑返回Stream<Character>(早期的原型可能实现了这个),但由于拳击开销而被拒绝.考虑到String将char值作为基元,当调用者可能只对该值进行一些处理并将其反转回字符串时,无条件地强加拳击似乎是错误的.

我们还考虑了一种CharStream原始的特化,但与它添加到API的批量相比,它的使用似乎相当狭窄.添加它似乎不值得.

对调用者施加的惩罚是他们必须知道IntStream包含的char值表示为ints和必须在适当的位置进行转换.这是令人困惑的,因为有过多的API调用,PrintStream.print(char)并且PrintStream.print(int)它们的行为明显不同.可能会出现另一个混乱点,因为codePoints()调用也返回一个IntStream但它包含的值非常不同.

因此,这归结为在几种选择中实际选择:

  1. 我们不能提供原始的特化,从而产生一个简单,优雅,一致的API,但它会带来高性能和GC开销;
  2. 我们可以提供一整套原始专业化,代价是混乱API并给JDK开发人员带来维护负担; 要么
  3. 我们可以提供一个原始特化的子集,给出一个中等大小,高性能的API,在相当窄范围的用例(字符处理)中给调用者带来相对较小的负担.

我们选择了最后一个


  • 只有三种系统自带的streams:IntStream, LongStream and DoubleStream.CharStream 并不存在,原因就是防止方法和类的爆炸,其他类型如(char、short、float)可以用它们更大的等价类型(int、double)来表示,同时也不会有显著的性能损失。已有的三个流的重载可能已经存在接口爆炸的现象了,如果八个接口都存在流类型,可能爆炸会非常严重。

  • 但我的建议是使用codePoints()而不是chars()你会发现很多库函数已经接受int除了char之外的代码点,例如的所有方法java.lang.Character以及StringBuilder.appendCodePoint等.这个支持从jdk1.5开始存在. 同时chars并不包含所有Unicode字符(他会切分代理对(UTF-16中用于扩展字符而使用的编码)),除非您确定不会是使用高位代码字符。

  • 关于代码点的好处.使用它们将处理补充字符,它们在Stringchar []中表示为代理对.大多数char处理代码极有可能错误处理了代理对.,这就是为什么Stream<Character>不应该存在了

  • 定义`void print(int ch){System.out.println((char)ch); 然后你可以使用方法引用,可能是一种办法

  • 自带的StreamCollectorcollect()方法.它们只有前面评论中提到的三参数collect()方法.可能代码要繁琐一点。使用代码点流(IntStream)也不错:collect(StringBuilder :: new,StringBuilder :: appendCodePoint,StringBuilder :: append).toString().我猜它不是很短,但使用代码点避免了(char)强制转换,并允许使用方法引用.此外,它正确处理代理人. 但是可以使用.collect(Collectors.joining())

  • 多亏了重复的问题,我注意到了这个.我同意chars()返回IntStream并不是一个大问题,特别是考虑到它很少使用这种方法的事实.然而,有一个内置的方法将IntStream转换回String会很好.它可以用.reduce(StringBuilder :: new,(sb,c) - > sb.append((char)c),StringBuilder :: append).toString()完成,但它确实很长.

  • 极简主义在这里出现.如果已经有chars()方法返回IntStream中的char值,那么为了获得相同的值但是以盒装形式的另一个API调用,它并没有增加太多.呼叫者可以毫不费力地打包值.当然,不必在这种(可能是罕见的)情况下执行此操作会更方便,但代价是为API添加混乱.

  • 但是,它没有回答为什么chars()不能有两个不同的方法,一个返回流Stream<Character>(性能损失很小),另一个是IntStream,这也被考虑了吗?如果人们认为便利性比性能损失更值得,那么他们很可能最终会将其映射到流Stream<Character>

  • (我个人可能会漏掉int但那只是我.)

  • int是唯一保证是原子的,不是长的和双精度的,所以并不对

  • 真正的说法是long和double的非原子性在流中并不真正相关,因为这些值都不会出现在跨线程共享的字段中。从纯API的角度来看,int不能做long也不能做的事情,所以从这个意义上说int是多余的。我怀疑使用IntStream的真正原因是性能,因为它只需要移动LongStream的一半数据。不过,我还没有测量过这个。

  • 非常不幸,这将花费开发人员数千小时的时间,因为像:array.stream(char[])这样的自然构造不起作用,开发人员将在谷歌上寻找替代方案。至少应该存在array.stream(char/byte/short[])并返回IntStream。

深入理解Java双冒号(::)运算符的使用

Jdk8中有好多新的特性,比如引入Lambda,简化代码的书写等等

Java8 推出了属于Java的lambda表达式,与一众的 => 不同,Java选择了 -> 做为箭头符号。有没有观众知道为什么这么选择。 lambda表达式的基本格式是这样的

1
( )->{ }

分析

java此时还有另外一个特性叫做lambda表达式和函数式接口,仅仅有一个未实现方法的接口,可以直接写作(参数列表) -> {方法体}这种形式。

例如:

1
2
3
4
@FunctionalInterface
public interface FuncA {
   void doSomeThing(String str);
}

那么上面这种接口就可以直接写作:

1
2
3
FuncA funcA = (str) -> {
  System.out.println("hello");
};

类似的还有Swing或者javaFx的监听器:

1
2
3
btn.addActionListener(e->{
  // do something
});

或者

java.lang.Iterable 的 foreach(xxx)方法中的xxx位置需要一个 Consumer 接口类

1
2
3
4
5
6
default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

而 Consumer 正好是一个函数式接口,@FunctionalInterface 注解和上面类似

1
2
3
4
5
6
@FunctionalInterface
public interface Consumer<T> {
    ...
    void accept(T t);
    ...
}

这样就省去了之前需要专为他编写一个实现类或者匿名内部类的代码,直接对接口进行实现。

而在这之上,如果一个方法的调用中,这个方法给接口提供的参数和他接收的返回,和你现有某个实现完全一致,就可以进一步进行简化,称为方法引用。

forEach方法提供一个某种类型的Object(具体是什么类型是要看Stream类的泛型参数的,不过一般就是这个集合提供的那种类型),而System.out.println可以接受一个Object,因此,forEach提供的参数和System.out.println的参数类型是一致的,可以进行这种简写。

具体来说就是:原本应该写为:

1
2
3
.forEach(element -> {
System.out.println(element)
})

但是System.out.println的参数和传递的参数element 的类型完全匹配,所以这样的时候就可以简化为:

1
.forEachSystem.out::println)

即forEach将会使用System.out对象的println方法进行接下来的操作。

那么对于Coutum接口可以这么用 Consumer one = (x) -> { System.out.println(x) } ,这句可以和上面结合起来看,x就是accept中的t,花括号里没有return是因为accept返回值是void。因为 Consumer 接口中只有一个方法,因此不会有冲突的可能。 然后再把 one 传进去forEach就可以了。

使用

我们先看一个关于Lambda的使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/**
* 输出list
 */
@Test
public void test() {
	String[] array = {"aaaa", "bbbb", "cccc"};
	List<String> list = Arrays.asList(array);
	
	//Java 7 
	for(String s:list){
		System.out.println(s);
	}
	
	//Java 8
	list.forEach(System.out::println);
}

其中list.forEach(System.out::println);就是Java 8中的Lambda写法之一, 有没有特别注意到输出语句跟我们平时写的syso语句不一样,常规输出语句是这样的:

1
System.out.println("流浪地球拍的不错哦!");

这里面使用到了::, 有点意思,来认识一下这个新东西!

双冒号(::)

英文:double colon,双冒号(::)运算符在Java 8中被用作方法引用(method reference),方法引用是与lambda表达式相关的一个重要特性。它提供了一种不执行方法的方法。为此,方法引用需要由兼容的函数接口组成的目标类型上下文。

Method References You use lambda expressions to create anonymous methods. Sometimes, however, a lambda expression does nothing but call an existing method. In those cases, it’s often clearer to refer to the existing method by name. Method references enable you to do this; they are compact, easy-to-read lambda expressions for methods that already have a name. 关于方法引用的描述,摘自oracle官网

大致意思是,使用lambda表达式会创建匿名方法, 但有时候需要使用一个lambda表达式只调用一个已经存在的方法(不做其它), 所以这才有了方法引用!

条件1

条件1为必要条件,必须要满足这个条件才能使用双冒号。

Lambda表达式内部只有一条表达式(第一种Lambda表达式),并且这个表达式只是调用已经存在的方法,不做其他的操作。

条件2

由于双冒号是为了省略item ->这一部分,所以条件2是需要满足不需要写参数item也知道如何使用item的情况。

有两种情况可以满足这个要求,这就是我将双冒号的使用分为2类的依据。

以下是Java 8中方法引用的一些语法:

  1. 静态方法引用(static method)语法:classname::methodname 例如:Person::getAge
  2. 对象的实例方法引用语法:instancename::methodname 例如:System.out::println
  3. 对象的超类方法引用语法: super::methodname
  4. 类构造器引用语法: classname::new 例如:ArrayList::new
  5. 数组构造器引用语法: typename[]::new 例如: String[]:new

总的来说lambda 表达式允许4种方式的双冒号

./9.jpg

如果上的语法太枯燥,那就通过一些例子来加强对它的理解:

静态方法语法使用例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.Arrays;
import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
 * 
 * @author zhoufy
 * @date 2019年2月20日 下午2:19:13
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ComponentScan("com.zhoufy")
public class Demo {
	@Test
	public void test() {
		List<String> list = Arrays.asList("aaaa", "bbbb", "cccc");
		
		//静态方法语法	ClassName::methodName
		list.forEach(Demo::print);
	}
	
	public static void print(String content){
		System.out.println(content);
	}
}

类实例方法语法使用例子:

其中的 Class 指的是类名,产生于 class Class { }。object 是实例对象,产生于 Class object = new Class(); 一个一个的说。

1
object::instanceMethod`  的一个典型用法就是 `System.out::println

在此请回看 Consumer 类的代码,就明白了为什么 Consumer one = System.out::printlnConsumer one = t -> System.out.println(t) 的简写

在这里插一个多线程的lambda表达式使用技巧:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package cn.edu.hubu.lhy;

public class SynchronizedTest {
    public synchronized void method1() {
        System.out.println("当前线程:" + Thread.currentThread());
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public synchronized void method1(String flag) {
        System.out.println("当前线程:" + Thread.currentThread());
        System.out.println("Method 1 start,flag=" + flag);
        try {
            System.out.println("Method 1 execute,flag=" + flag);
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end,flag=" + flag);
    }

    public synchronized void method2() {
        System.out.println("当前线程:" + Thread.currentThread());
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public synchronized void method2(String flag) {
        System.out.println("当前线程:" + Thread.currentThread());
        System.out.println("Method 2 start,flag=" + flag);
        try {
            System.out.println("Method 2 execute,flag=" + flag);
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end,flag=" + flag);
    }

    public static void main(String[] args) {
        // 一种最简洁的启动方法
//        new Thread( ()-> new SynchronizedTest().method1() ).start();
        //新建实例对象的启动方法
        final SynchronizedTest test = new SynchronizedTest();

        Thread t1 = new Thread(test::method1);
        System.out.println("t1:" + t1);
        t1.start();

        Thread t2 = new Thread(() -> test.method1("1"));
        System.out.println("t2:" + t2);
        t2.start();
//
//        显式的赋值Runnable后再启动
        Runnable tt = test::method1;
        Thread t3 = new Thread(tt);
        System.out.println("t3:" + t3);
        t3.start();

        Runnable tt2 = () -> test.method1("1");
        Thread t4 = new Thread(tt2);
        System.out.println("t4:" + t4);
        t4.start();
        //
        Thread t5 = new Thread(test::method2);
        System.out.println("t5:" + t5);
        t5.start();

        Thread t6 = new Thread(() -> test.method2("1"));
        System.out.println("t6:" + t6);
        t6.start();
//
//        显式的赋值Runnable后再启动
        Runnable tt3 = test::method2;
        Thread t7 = new Thread(tt3);
        System.out.println("t7:" + t7);
        t7.start();

        Runnable tt4 = () -> test.method2("1");
        Thread t8 = new Thread(tt4);
        System.out.println("t4:" + t8);
        t8.start();

        //运行完后对结果有没有疑问? 为什么2后面还有1?
        //线程不设置flag,无为而治的执行顺序为先执行第一个线程,然后从最后一个线程倒序执行到第二个线程
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.Arrays;
import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
 * 
 * @author zhoufy
 * @date 2019年2月20日 下午2:19:13
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ComponentScan("com.zhoufy")
public class Demo {
	@Test
	public void test() {
		List<String> list = Arrays.asList("aaaa", "bbbb", "cccc");
		
		//对象实例语法	instanceRef::methodName
		list.forEach(new Demo()::print);
	}
	
	public void print(String content){
		System.out.println(content);
	}
}

超类方法语法使用例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.Arrays;
import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;


/**
 * @author zhoufy
 * @date 2019年2月20日 下午2:41:38
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ComponentScan("com.zhoufy")
public class Example extends BaseExample{

	@Test
	public void test() {
		List<String> list = Arrays.asList("aaaa", "bbbb", "cccc");
		
		//对象的超类方法语法: super::methodName 
		list.forEach(super::print);
	}
}

class BaseExample {
	public void print(String content){
		System.out.println(content);
	}
}

类构造器语法使用例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
 * 
 * @author zhoufy
 * @date 2019年2月20日 下午2:19:13
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ComponentScan("com.zhoufy")
public class Example {

	@Test
	public void test() {
		InterfaceExample com =  Example::new;
		Example bean = com.create();	
		System.out.println(bean);
	}
}

interface InterfaceExample{
	Example create();
}

如果是带参数的构造器,示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * @author zhoufy
 * @date 2019年2月20日 下午2:19:13
 */
public class Example {
	
	private String name;
	
	Example(String name){
		this.name = name;
	}
	
	public static void main(String[] args) {
		InterfaceExample com =  Example::new;
		Example bean = com.create("hello world");
		System.out.println(bean.name);
	}
}
interface InterfaceExample{
	Example create(String name);
}

这里需要特别注意的是:Example 类并没有implements InterfaceExample接口哦!!!

数组构造器语法使用例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import java.util.function.Function;

/**
 * @author zhoufy
 * @date 2019年2月20日 下午2:19:13
 */
public class Example {
	public static void main(String[] args) {
		Function <Integer, Example[]> function = Example[]::new;
		Example[] array = function.apply(4);	//这里的4是数组的大小
		
		for(Example e:array){
			System.out.println(e);	//如果输出的话,你会发现会输出4个空对象(null)
		}
	}
}

这里是借用jdk自带的java.util.function.Function类实现的,如果想要自定义接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 
 * @author zhoufy
 * @date 2019年2月20日 下午2:19:13
 */
public class Example {
	
	public static void main(String[] args) {
		Interface <Integer, Example[]> function = Example[]::new;
		Example[] array = function.apply(4);	//这里的4是数组的大小
		
		for(Example e:array){
			System.out.println(e);
		}
	}
}

@FunctionalInterface
interface Interface<A, T>{
	T apply(A a); 
}

自定义接口的几种调用方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
interface Person{
    void say(String t);
}

class AnimalSay{  
     public static void saySomething(String something,Person person) {
         person.say(something);
     }
}

public class Mian {
     public static void main(String[] args) {  
         Person xiaoMing = (x) -> { System.out.println(x); };    //1
         //xiaoMing.say("hello");
         AnimalSay.saySomething("hello", xiaoMing );           //2
         //AnimalSay.saySomething("hello", (x)->{System.out.println(x)};);    //3
     }
}

可以直接把 lambda 表达式直接放在需要的位置。这时候 lambda 就可以自动变成所需类 (上面代码被注掉的部分)。

那么回到最开始也有两种用法

1
2
3
4
5
//第一种
Consumer one = (x) -> { System.out.println(x);};
Iterable.foreach(one);
//第二种
Iterable.foreach( x->System.out.println(x) );

Java双端队列Deque使用详解

./10.jpg

Deque是一个双端队列接口,继承自Queue接口,Deque的实现类是LinkedList、ArrayDeque、LinkedBlockingDeque,其中LinkedList是最常用的。

Deque有三种用途:

  • 普通队列(一端进另一端出): Queue queue = new LinkedList()或Deque deque = new LinkedList()
  • 双端队列(两端都可进出) Deque deque = new LinkedList()
  • 堆栈 Deque deque = new LinkedList()

注意:Java堆栈Stack类已经过时,Java官方推荐使用Deque替代Stack使用。Deque堆栈操作方法:push()、pop()、peek()。

Deque是一个线性collection,支持在两端插入和移除元素。名称 deque 是“double ended queue(双端队列)”的缩写,通常读为“deck”。大多数 Deque 实现对于它们能够包含的元素数没有固定限制,但此接口既支持有容量限制的双端队列,也支持没有固定大小限制的双端队列。

此接口定义在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false,具体取决于操作)。插入操作的后一种形式是专为使用有容量限制的 Deque 实现设计的;在大多数实现中,插入操作不能失败。

下表总结了上述 12 种方法:

第一个元素 (头部) 最后一个元素 (尾部)
抛出异常 特殊值 抛出异常 特殊值
插入 addFirst(e) offerFirst(e) addLast(e) offerLast(e)
删除 removeFirst() pollFirst() removeLast() pollLast()
检查 getFirst() peekFirst() getLast() peekLast()

Deque接口扩展(继承)了 Queue 接口。在将双端队列用作队列时,将得到 FIFO(先进先出)行为。将元素添加到双端队列的末尾,从双端队列的开头移除元素。从 Queue 接口继承的方法完全等效于 Deque 方法,如下表所示:

Queue方法 等效Deque方法
add add(e) addLast(e)
offer(e) offerLast(e)
remove() removeFirst()
poll() pollFirst()
element() getFirst()
peek() peekFirst()

双端队列也可用作 LIFO(后进先出)堆栈。应优先使用此接口而不是遗留 Stack 类。在将双端队列用作堆栈时,元素被推入双端队列的开头并从双端队列开头弹出。堆栈方法完全等效于 Deque 方法,如下表所示:

堆栈方法 等效Deque方法
push(e) addFirst(e)
pop() removeFirst()
peek() peekFirst()

Queue队列操作

方法名 作用 区别
add 增加一个元索 如果队列已满,则抛出一个IIIegaISlabEepeplian异常
remove 移除并返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
element 返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
offer 添加一个元素并返回true 如果队列已满,则返回false
offer 移除并返问队列头部的元素 如果队列为空,则返回null
peek 返回队列头部的元素 如果队列为空,则返回null
put 添加一个元素 如果队列满,则阻塞
take 移除并返回队列头部的元素 如果队列为空,则阻塞

HashSet 是否无序

(一) 问题起因:

《Core Java Volume I—Fundamentals》中对HashSet的描述是这样的:

HashSet:一种没有重复元素的无序集合

解释:我们一般说HashSet是无序的,它既不能保证存储和取出顺序一致,更不能保证自然顺序(a-z)

下面是《Thinking in Java》中的使用Integer对象的HashSet的示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  import java.util.*;
  
  public class SetOfInteger {
      public static void main(String[] args) {
          Random rand = new Random(47);
          Set<Integer> intset = new HashSet<Integer>();
          for (int i = 0; i<10000; i++)
              intset.add(rand.nextInt(30));
          System.out.println(intset);
  
      }
  }
/* Output:
  [15, 8, 23, 16, 7, 22, 9, 21, 6, 1 , 29 , 14, 24, 4, 19, 26, 11, 18, 3, 12, 27, 17, 2, 13, 28, 20, 25, 10, 5, 0]

在0-29之间的10000个随机数被添加到了Set中,大量的数据是重复的,但输出结果却每一个数只有一个实例出现在结果中,并且输出的结果没有任何规律可循。 这正与其不重复,且无序的特点相吻合。

看来两本书的结果,以及我们之前所学的知识,看起来都是一致的,一切就是这么美好。

随手运行了一下这段书中的代码,结果却让人大吃一惊

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  //JDK1.8下 Idea中运行
  import java.util.*;
  
  public class SetOfInteger {
      public static void main(String[] args) {
          Random rand = new Random(47);
          Set<Integer> intset = new HashSet<Integer>();
          for (int i = 0; i<10000; i++)
              intset.add(rand.nextInt(30));
          System.out.println(intset);
      }
  }
  
  //运行结果
  [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]

嗯!不重复的特点依旧吻合,但是为什么遍历输出结果却是有序的???

写一个最简单的程序再验证一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  import java.util.*;
  
  public class HashSetDemo {
      public static void main(String[] args) {
  
          Set<Integer> hs = new HashSet<Integer>();
  
          hs.add(1);
          hs.add(2);
          hs.add(3);
  
          //增强for遍历
          for (Integer s : hs) {
              System.out.print(s + " ");
          }
      }
  }
  
  //运行结果
  1 2 3 

我还不死心,是不是元素数据不够多,有序这只是一种巧合的存在,增加元素数量试试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  import java.util.*;
  
  public class HashSetDemo {
      public static void main(String[] args) {
          
          Set<Integer> hs = new HashSet<Integer>();
  
          for (int i = 0; i < 10000; i++) {
              hs.add(i);
          }
  
          //增强for遍历
          for (Integer s : hs) {
              System.out.print(s + " ");
          }
      }
  }
  
  //运行结果
  1 2 3 ... 9997 9998 9999 

可以看到,遍历后输出依旧是有序的

(二) 过程

通过一步一步分析源码,我们来看一看,这究竟是怎么一回事,首先我们先从程序的第一步——集合元素的存储开始看起,先看一看HashSet的add方法源码:

1
2
3
4
  // HashSet 源码节选-JKD8
  public boolean add(E e) {
      return map.put(e, PRESENT)==null;
  }

我们可以看到,HashSet直接调用HashMap的put方法,并且将元素e放到map的key位置(保证了唯一性 )

顺着线索继续查看HashMap的put方法源码:

1
2
3
4
  //HashMap 源码节选-JDK8
  public V put(K key, V value) {
      return putVal(hash(key), key, value, false, true);
  }

而我们的值在返回前需要经过HashMap中的hash方法

接着定位到hash方法的源码:

1
2
3
4
5
  //HashMap 源码节选-JDK8
  static final int hash(Object key) {
      int h;
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

hash方法的返回结果中是一句三目运算符,键 (key) 为null即返回 0,存在则返回后一句的内容

1
  (h = key.hashCode()) ^ (h >>> 16)

JDK8中 HashMap——hash 方法中的这段代码叫做 “扰动函数

我们来分析一下:

hashCode是Object类中的一个方法,在子类中一般都会重写,而根据我们之前自己给出的程序,暂以Integer类型为例,我们来看一下Integer中hashCode方法的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  /**
   * Returns a hash code for this {@code Integer}.
   *
   * @return  a hash code value for this object, equal to the
   *          primitive {@code int} value represented by this
   *          {@code Integer} object.
   */
  @Override
  public int hashCode() {
      return Integer.hashCode(value);
  }
  
  /**
   * Returns a hash code for a {@code int} value; compatible with
   * {@code Integer.hashCode()}.
   *
   * @param value the value to hash
   * @since 1.8
   *
   * @return a hash code value for a {@code int} value.
   */
  public static int hashCode(int value) {
      return value;
  }

Integer中hashCode方法的返回值就是这个数本身

注:整数的值因为与整数本身一样唯一,所以它是一个足够好的散列

所以,下面的A、B两个式子就是等价的

1
2
3
4
5
  //注:key为 hash(Object key)参数
  
  A(h = key.hashCode()) ^ (h >>> 16)
  
  Bkey ^ (key >>> 16)

分析到这一步,我们的式子只剩下位运算了,先不急着算什么,我们先理清思路

HashSet因为底层使用哈希表链表结合数组)实现,存储时key通过一些运算后得出自己在数组中所处的位置。

我们在hashCoe方法中返回到了一个等同于本身值的散列值,但是考虑到int类型数据的范围:-2147483648~2147483647 ,着很显然,这些散列值不能直接使用,因为内存是没有办法放得下,一个40亿长度的数组的。所以它使用了对数组长度进行取模运算,得余后再作为其数组下标,indexFor() ——JDK7中,就这样出现了,在JDK8中 indexFor()就消失了,而全部使用下面的语句代替,原理是一样的。

1
2
  //JDK8中
  (tab.length - 1) & hash
1
2
3
4
5
6
  //JDK7中
  bucketIndex = indexFor(hash, table.length);
  
  static int indexFor(int h, int length) {
      return h & (length - 1);
  }

提一句,为什么取模运算时我们用 & 而不用 % 呢,因为位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快,这样就导致位运算 & 效率要比取模运算 % 高很多。

看到这里我们就知道了,存储时key需要通过hash方法indexFor()运算,来确定自己的对应下标

(取模运算,应以JDK8为准,但为了称呼方便,还是按照JDK7的叫法来说,下面的例子均为此,特此提前声明)

但是先直接看与运算(&),好像又出现了一些问题,我们举个例子:

HashMap中初始长度为16,length - 1 = 15;其二进制表示为 00000000 00000000 00000000 00001111

而与运算计算方式为:遇0则0,我们随便举一个key值

1
2
3
4
          1111 1111 1010 0101 1111 0000 0011 1100
  &       0000 0000 0000 0000 0000 0000 0000 1111
  ----------------------------------------------------
          0000 0000 0000 0000 0000 0000 0000 1100

我们将这32位从中分开,左边16位称作高位,右边16位称作低位,可以看到经过&运算后 结果就是高位全部归0,剩下了低位的最后四位。但是问题就来了,我们按照当前初始长度为默认的16,HashCode值为下图两个,可以看到,在不经过扰动计算时,只进行与(&)运算后 Index值均为 12 这也就导致了哈希冲突

./18.jpg

哈希冲突的简单理解:计划把一个对象插入到散列表(哈希表)中,但是发现这个位置已经被别的对象所占据了

例子中,两个不同的HashCode值却经过运算后,得到了相同的值,也就代表,他们都需要被放在下标为2的位置

一般来说,如果数据分布比较广泛,而且存储数据的数组长度比较大,那么哈希冲突就会比较少,否则很高。

但是,如果像上例中只取最后几位的时候,这可不是什么好事,即使我的数据分布很散乱,但是哈希冲突仍然会很严重。

别忘了,我们的扰动函数还在前面搁着呢,这个时候它就要发挥强大的作用了,还是使用上面两个发生了哈希冲突的数据,这一次我们加入扰动函数再进行与(&)运算

./19.jpg

补充 :>>> 按位右移补零操作符,左操作数的值按右操作数指定的为主右移,移动得到的空位以零填充 ​ ^ 位异或运算,相同则0,不同则1

可以看到,本发生了哈希冲突的两组数据,经过扰动函数处理后,数值变得不再一样了,也就避免了冲突

其实在扰动函数中,将数据右位移16位,哈希码的高位和低位混合了起来,这也正解决了前面所讲 高位归0,计算只依赖低位最后几位的情况, 这使得高位的一些特征也对低位产生了影响,使得低位的随机性加强,能更好的避免冲突

到这里,我们一步步研究到了这一些知识

1
  HashSet add()  HashMap put()  HashMap hash()  HashMap (tab.length - 1) & hash

有了这些知识的铺垫,我对于刚开始自己举的例子又产生了一些疑惑,我使用for循环添加一些整型元素进入集合,难道就没有任何一个发生哈希冲突吗,为什么遍历结果是有序输出的,经过简单计算 2 和18这两个值就都是2

(这个疑惑是有问题的,后面解释了错在了哪里)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  //key = 2,(length -1) = 15
  
  h = key.hashCode()      0000 0000 0000 0000 0000 0000 0000 0010 
  h >>> 16                0000 0000 0000 0000 0000 0000 0000 0000
  hash = h^(h >>> 16)     0000 0000 0000 0000 0000 0000 0000 0010
  (tab.length-1)&hash     0000 0000 0000 0000 0000 0000 0000 1111
                          0000 0000 0000 0000 0000 0000 0000 0010  
  -------------------------------------------------------------
                          0000 0000 0000 0000 0000 0000 0000 0010
  
  //2的十进制结果:2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  //key = 18,(length -1) = 15
  
  h = key.hashCode()      0000 0000 0000 0000 0000 0000 0001 0010 
  h >>> 16                0000 0000 0000 0000 0000 0000 0000 0000
  hash = h^(h >>> 16)     0000 0000 0000 0000 0000 0000 0001 0010
  (tab.length-1)&hash     0000 0000 0000 0000 0000 0000 0000 1111
                          0000 0000 0000 0000 0000 0000 0000 0010  
  -------------------------------------------------------------
                          0000 0000 0000 0000 0000 0000 0000 0010
  
  //18的十进制结果:2

按照我们上面的知识,按理应该输出 1 2 18 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 但却仍有序输出了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  import java.util.*;
  
  public class HashSetDemo {
      public static void main(String[] args) {
  
          Set<Integer> hs = new HashSet<Integer>();
  
          for (int i = 0; i < 19; i++) {
              hs.add(i);
          }
  
          //增强for遍历
          for (Integer s : hs) {
              System.out.print(s + " ");
          }
      }
  }
  
  //运行结果:
  0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 

再试一试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  import java.util.*;
  
  public class HashSetDemo {
      public static void main(String[] args) {
  
          Set<Integer> hs = new HashSet<Integer>();
          
          hs.add(0)
          hs.add(1);
          hs.add(18);
          hs.add(2);
          hs.add(3);
          hs.add(4);
          ......
          hs.add(17)
  
          //增强for遍历
          for (Integer s : hs) {
              System.out.print(s + " ");
          }
      }
  }
  
  //运行结果:
  0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

真让人头大,不死心再试一试,由与偷懒,就只添加了几个,就是这个偷懒,让我发现了新大陆!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
  import java.util.*;
  
  public class HashSetDemo {
      public static void main(String[] args) {
  
          Set<Integer> hs = new HashSet<Integer>();
  
          hs.add(1);
          hs.add(18);
          hs.add(2);
          hs.add(3);
          hs.add(4);
  
          //增强for遍历
          for (Integer s : hs) {
              System.out.print(s + " ");
          }
      }
  }
  
  //运行结果:
  1 18 2 3 4

这一段程序按照我们认为应该出现的顺序出现了!!!

突然恍然大悟,我忽略了最重要的一个问题,也就是数组长度问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  //HashMap 源码节选-JDK8
  /**
  * The default initial capacity - MUST be a power of two.
  */
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  
  /**
  * The load factor used when none specified in constructor.
  */
  static final float DEFAULT_LOAD_FACTOR = 0.75f;

« :按位左移运算符,做操作数按位左移右错作数指定的位数,即左边最高位丢弃,右边补齐0,计算的简便方法就是:把 « 左面的数据乘以2的移动次幂 为什么初始长度为16:1 « 4 即 1 * 2 ^4 =16;

我们还观察到一个叫做加载因子的东西,他默认值为0.75f,这是什么意思呢,我们来补充一点它的知识:

加载因子就是表示哈希表中元素填满的程度,当表中元素过多,超过加载因子的值时,哈希表会自动扩容,一般是一倍,这种行为可以称作rehashing(再哈希)。 加载因子的值设置的越大,添加的元素就会越多,确实空间利用率的到了很大的提升,但是毫无疑问,就面临着哈希冲突的可能性增大,反之,空间利用率造成了浪费,但哈希冲突也减少了,所以我们希望在空间利用率与哈希冲突之间找到一种我们所能接受的平衡,经过一些试验,定在了0.75f

现在可以解决我们上面的疑惑了

数组初始的实际长度 = 16 * 0.75 = 12

这代表当我们元素数量增加到12以上时就会发生扩容,当我们上例中for循环添加0-18, 这19个元素时,先保存到前12个到第十三个元素时,超过加载因子,导致数组发生了一次扩容,而扩容以后对应与(&)运算的(tab.length-1)就发生了变化,从16-1 变成了 32-1 即31

我们来算一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  //key = 2,(length -1) = 31
  
  h = key.hashCode()      0000 0000 0000 0000 0000 0000 0001 0010 
  h >>> 16                0000 0000 0000 0000 0000 0000 0000 0000
  hash = h^(h >>> 16)     0000 0000 0000 0000 0000 0000 0001 0010
  (tab.length-1)&hash     0000 0000 0000 0000 0000 0000 0011 1111 
                          0000 0000 0000 0000 0000 0000 0000 0010      
  -------------------------------------------------------------
                          0000 0000 0000 0000 0000 0000 0000 0010
  
  //十进制结果:2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  //key = 18,(length -1) = 31
  
  h = key.hashCode()      0000 0000 0000 0000 0000 0000 0001 0010 
  h >>> 16                0000 0000 0000 0000 0000 0000 0000 0000
  hash = h^(h >>> 16)     0000 0000 0000 0000 0000 0000 0001 0010
  (tab.length-1)&hash     0000 0000 0000 0000 0000 0000 0011 1111 
                          0000 0000 0000 0000 0000 0000 0000 0010      
  -------------------------------------------------------------
                          0000 0000 0000 0000 0000 0000 0001 0010
  
  //十进制结果:18

当length - 1 的值发生改变的时候,18的值也变成了本身。

到这里,才意识到自己之前用2和18计算时 均使用了 length -1 的值为 15是错误的,当时并不清楚加载因子及它的扩容机制,这才是导致提出有问题疑惑的根本原因。

(三) 总结

JDK7到JDK8,其内部发生了一些变化,导致在不同版本JDK下运行结果不同,根据上面的分析,我们从HashSet追溯到HashMap的hash算法、加载因子和默认长度。

由于我们所创建的HashSet是Integer类型的,这也是最巧的一点,Integer类型hashCode()的返回值就是其int值本身,而存储的时候元素通过一些运算后会得出自己在数组中所处的位置。由于在这一步,其本身即下标(只考虑这一步),其实已经实现了排序功能,由于int类型范围太广,内存放不下,所以对其进行取模运算,为了减少哈希冲突,又在取模前进行了,扰动函数的计算,得到的数作为元素下标,按照JDK8下的hash算法,以及load factor及扩容机制,这就导致数据在经过 HashMap.hash()运算后仍然是自己本身的值,且没有发生哈希冲突。

补充:对于有序无序的理解

集合所说的序,是指元素存入集合的顺序,当元素存储顺序和取出顺序一致时就是有序,否则就是无序。

并不是说存储数据的时候无序,没有规则,当我们不论使用for循环随机数添加元素的时候,还是for循环有序添加元素的时候,最后遍历输出的结果均为按照值的大小排序输出,随机添加元素,但结果仍有序输出,这就对照着上面那句,存储顺序和取出顺序是不一致的,所以我们说HashSet是无序的,虽然我们按照123的顺序添加元素,结果虽然仍为123,但这只是一种巧合而已。

所以HashSet只是不保证有序,并不是保证无序

Java中List, Integer[], int[]的相互转换

相信新手们在学习Java的过程中都会遇到和我一样的问题:想要把List和int[]相互转换太麻烦了。

List和String[]也同理。难道每次非得写一个循环遍历吗?其实一步就可以搞定。

本文涉及到一些Java8的特性。如果没有接触过就先学会怎么用,然后再细细研究。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
 
public class Main {
    public static void main(String[] args) {
        int[] data = {4, 5, 3, 6, 2, 5, 1};
 
        // int[] 转 List<Integer>
        List<Integer> list1 = Arrays.stream(data).boxed().collect(Collectors.toList());
        // Arrays.stream(arr) 可以替换成IntStream.of(arr)。
        // 1.使用Arrays.stream将int[]转换成IntStream。
        // 2.使用IntStream中的boxed()装箱。将IntStream转换成Stream<Integer>。
        // 3.使用Stream的collect(),将Stream<T>转换成List<T>,因此正是List<Integer>。
 
        // int[] 转 Integer[]
        Integer[] integers1 = Arrays.stream(data).boxed().toArray(Integer[]::new);
        // 前两步同上,此时是Stream<Integer>。
        // 然后使用Stream的toArray,传入IntFunction<A[]> generator。
        // 这样就可以返回Integer数组。
        // 不然默认是Object[]。
 
        // List<Integer> 转 Integer[]
        Integer[] integers2 = list1.toArray(new Integer[0]);
        //  调用toArray。传入参数T[] a。这种用法是目前推荐的。
        // List<String>转String[]也同理。
 
        // List<Integer> 转 int[]
        int[] arr1 = list1.stream().mapToInt(Integer::valueOf).toArray();
        // 想要转换成int[]类型,就得先转成IntStream。
        // 这里就通过mapToInt()把Stream<Integer>调用Integer::valueOf来转成IntStream
        // 而IntStream中默认toArray()转成int[]。
 
        // Integer[] 转 int[]
        int[] arr2 = Arrays.stream(integers1).mapToInt(Integer::valueOf).toArray();
        // 思路同上。先将Integer[]转成Stream<Integer>,再转成IntStream。
 
        // Integer[] 转 List<Integer>
        List<Integer> list2 = Arrays.asList(integers1);
        // 最简单的方式。String[]转List<String>也同理。
 
        // 同理
        String[] strings1 = {"a", "b", "c"};
        // String[] 转 List<String>
        List<String> list3 = Arrays.asList(strings1);
        // List<String> 转 String[]
        String[] strings2 = list3.toArray(new String[0]);
 
    }
}

由此可见,流操作还是很方便的。我对这些知识的掌握也不深,就不误人子弟了,只是给刚接触的人提供一个思考的方向。

数组操作

1
2
说明:ArrayUtils工具类在标准的应用程序中是不可以被实例化的:
参考:[参考地址](http://commons.apache.org/proper/commons-lang/javadocs/api-release/)

fill

数组的初始化填充

1
2
3
int[] iL = new int[100];
//将数组填充为100个-1
Arrays.fill(iL,-1)

ArrayUtils

数组工具ArrayUtils 拥有以下方法:

toString

将一个数组转换成String,用于打印数组

isEquals

判断两个数组是否相等,采用EqualsBuilder进行判断

toMap

将一个数组转换成Map,如果数组里是Entry则其Key与Value就是新Map的Key和Value,如果是Object[]则Object[0]为KeyObject[1]为Value

clone

拷贝数组

subarray

截取子数组

isSameLength

判断两个数组长度是否相等

getLength

获得数组的长度

isSameType

判段两个数组的类型是否相同

reverse

数组反转

indexOf

查询某个Object在数组中的位置,可以指定起始搜索位置

lastIndexOf

反向查询某个Object在数组中的位置,可以指定起始搜索位置

contains

查询某个Object是否在数组中

toObject

将基本数据类型转换成外包型数据

isEmpty

判断数组是否为空(null和length=0的时候都为空)

addAll

合并两个数组

add

添加一个数据到数组

remove

删除数组中某个位置上的数据

removeElement

删除数组中某个对象(从正序开始搜索,删除第一个)

eg:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import org.apache.commons.lang3.ArrayUtils
// 1.打印数组
ArrayUtils.toString(new int[] { 1, 4, 2, 3 });// {1,4,2,3}
ArrayUtils.toString(new Integer[] { 1, 4, 2, 3 });// {1,4,2,3}
ArrayUtils.toString(null, "I'm nothing!");// I'm nothing!

// 2.判断两个数组是否相等,采用EqualsBuilder进行判断
// 只有当两个数组的数据类型,长度,数值顺序都相同的时候,该方法才会返回True
// 2.1 两个数组完全相同
ArrayUtils.isEquals(new int[] { 1, 2, 3 }, new int[] { 1, 2, 3 });// true
// 2.2 数据类型以及长度相同,但各个Index上的数据不是一一对应
ArrayUtils.isEquals(new int[] { 1, 3, 2 }, new int[] { 1, 2, 3 });// true
// 2.3 数组的长度不一致
ArrayUtils.isEquals(new int[] { 1, 2, 3, 3 }, new int[] { 1, 2, 3 });// false
// 2.4 不同的数据类型
ArrayUtils.isEquals(new int[] { 1, 2, 3 }, new long[] { 1, 2, 3 });// false
ArrayUtils.isEquals(new Object[] { 1, 2, 3 }, new Object[] { 1, (long) 2, 3 });// false
// 2.5 Null处理,如果输入的两个数组都为null时候则返回true
ArrayUtils.isEquals(new int[] { 1, 2, 3 }, null);// false
ArrayUtils.isEquals(null, null);// true

// 3.将一个数组转换成Map
// 如果数组里是Entry则其Key与Value就是新Map的Key和Value,如果是Object[]则Object[0]为KeyObject[1]为Value
// 对于Object[]数组里的元素必须是instanceof Object[]或者Entry,即不支持基本数据类型数组
// 如:ArrayUtils.toMap(new Object[]{new int[]{1,2},new int[]{3,4}})会出异常
ArrayUtils.toMap(new Object[] { new Object[] { 1, 2 }, new Object[] { 3, 4 } });// {1=2,
// 3=4}
ArrayUtils.toMap(new Integer[][] { new Integer[] { 1, 2 }, new Integer[] { 3, 4 } });// {1=2,
// 3=4}

// 4.拷贝数组
ArrayUtils.clone(new int[] { 3, 2, 4 });// {3,2,4}

// 5.截取数组
ArrayUtils.subarray(new int[] { 3, 4, 1, 5, 6 }, 2, 4);// {1,5}
// 起始index为2(即第三个数据)结束index为4的数组
ArrayUtils.subarray(new int[] { 3, 4, 1, 5, 6 }, 2, 10);// {1,5,6}
// 如果endIndex大于数组的长度,则取beginIndex之后的所有数据

// 6.判断两个数组的长度是否相等
ArrayUtils.isSameLength(new Integer[] { 1, 3, 5 }, new Long[] { 2L, 8L, 10L });// true

// 7.获得数组的长度
ArrayUtils.getLength(new long[] { 1, 23, 3 });// 3

// 8.判段两个数组的类型是否相同
ArrayUtils.isSameType(new long[] { 1, 3 }, new long[] { 8, 5, 6 });// true
ArrayUtils.isSameType(new int[] { 1, 3 }, new long[] { 8, 5, 6 });// false

// 9.数组反转
int[] array = new int[] { 1, 2, 5 };
ArrayUtils.reverse(array);// {5,2,1}

// 10.查询某个Object在数组中的位置,可以指定起始搜索位置,找不到返回-1
// 10.1 从正序开始搜索,搜到就返回当前的index否则返回-1
ArrayUtils.indexOf(new int[] { 1, 3, 6 }, 6);// 2
ArrayUtils.indexOf(new int[] { 1, 3, 6 }, 2);// -1
// 10.2 从逆序开始搜索,搜到就返回当前的index否则返回-1
ArrayUtils.lastIndexOf(new int[] { 1, 3, 6 }, 6);// 2

// 11.查询某个Object是否在数组中
ArrayUtils.contains(new int[] { 3, 1, 2 }, 1);// true
// 对于Object数据是调用该Object.equals方法进行判断
ArrayUtils.contains(new Object[] { 3, 1, 2 }, 1L);// false

// 12.基本数据类型数组与外包型数据类型数组互转
ArrayUtils.toObject(new int[] { 1, 2 });// new Integer[]{Integer,Integer}
ArrayUtils.toPrimitive(new Integer[] { new Integer(1), new Integer(2) });// new int[]{1,2}

// 13.判断数组是否为空(null和length=0的时候都为空)
ArrayUtils.isEmpty(new int[0]);// true
ArrayUtils.isEmpty(new Object[] { null });// false

// 14.合并两个数组
ArrayUtils.addAll(new int[] { 1, 3, 5 }, new int[] { 2, 4 });// {1,3,5,2,4}

// 15.添加一个数据到数组
ArrayUtils.add(null, '0')       //= ['0']
ArrayUtils.add(['1'], '0')      //= ['1', '0']
ArrayUtils.add(['1', '0'], '1') //= ['1', '0', '1']
//说明:在给定的数组副本中加入传入的数组和给定的元素,如果给定的数组是null,那么会返回一个包含给定元素的数组;
//参数:array-要被复制的数组;element-在新数组中的最后一个索引处添加的元素;
public static char[] add(char[] array, char element)

ArrayUtils.add(null, 0)   //= [0]
ArrayUtils.add([1], 0)    //= [1, 0]
ArrayUtils.add([1, 0], 1) //= [1, 0, 1]
//说明:在给定的数组副本中加入传入的数组和给定的元素,如果给定的数组是null,那么会返回一个包含给定元素的数组;
//参数:array-要被复制的数组;element-在新数组中的最后一个索引处添加的元素;
public static double[] add(double[] array, double element)

ArrayUtils.add(null, 0)   //= [0]
ArrayUtils.add([1], 0)    //= [1, 0]
ArrayUtils.add([1, 0], 1) //= [1, 0, 1]
//说明:在给定的数组副本中加入传入的数组和给定的元素,如果给定的数组是null,那么会返回一个包含给定元素的数组;
//参数:array-要被复制的数组;element-在新数组中的最后一个索引处添加的元素;
public static float[] add(float[] array, float element)

ArrayUtils.add(new int[] { 1, 3, 5 }, 4);// {1,3,5,4}
ArrayUtils.add(null, 0)                  //= [0]
ArrayUtils.add([1], 0)                   //= [1, 0]
ArrayUtils.add([1, 0], 1)                //= [1, 0, 1]
//说明:在给定的数组副本中加入传入的数组和给定的元素,如果给定的数组是null,那么会返回一个包含给定元素的数组;
//参数:array-要被复制的数组;element-在新数组中的最后一个索引处添加的元素;
public static int[] add(int[] array, int element)

ArrayUtils.add(null, 0)   //= [0]
ArrayUtils.add([1], 0)    //= [1, 0]
ArrayUtils.add([1, 0], 1) //= [1, 0, 1] 
//说明:在给定的数组副本中加入传入的数组和给定的元素,如果给定的数组是null,那么会返回一个包含给定元素的数组;
//参数:array-要被复制的数组;element-在新数组中的最后一个索引处添加的元素;
public static long[] add(long[] array, long element)

ArrayUtils.add(null, 0)   //= [0]
ArrayUtils.add([1], 0)    //= [1, 0]
ArrayUtils.add([1, 0], 1) //= [1, 0, 1]
//说明:在给定的数组副本中加入传入的数组和给定的元素,如果给定的数组是null,那么会返回一个包含给定元素的数组;
//参数:array-要被复制的数组;element-在新数组中的最后一个索引处添加的元素;
public static short[] add(short[] array, short element)

ArrayUtils.add(null, 0)   //= [0]
ArrayUtils.add([1], 0)    //= [1, 0]
ArrayUtils.add([1, 0], 1) //= [1, 0, 1]
//说明:java的基本数据类型中有byte这种,byte存储整型数据,占据1个字节(8 bits),能够存储的数据范围是-128~+127;在给定的数组副本中加入传入的数组和给定的元素,如果给定的数组是null,那么会返回一个包含给定元素的数组;
//参数:array-要被复制的数组;element-在新数组中的最后一个索引处添加的元素;
public static byte[] add(byte[] array, byte element)
    
ArrayUtils.add(null, true)          //= [true]
ArrayUtils.add([true], false)       //= [true, false]
ArrayUtils.add([true, false], true) //= [true, false, true]
//说明:在给定的数组副本中加入传入的数组和给定的元素,如果给定的数组是null,那么会返回一个包含给定元素的数组;
//参数:array-要被复制的数组;element-在新数组中的最后一个索引处添加的元素;
public static boolean[] add(boolean[] array, boolean element)


// 16.删除数组中某个位置上的数据
ArrayUtils.remove(new int[] { 1, 3, 5 }, 1);// {1,5}
ArrayUtils.remove([1], 0)                   //= []
ArrayUtils.remove([2, 6], 0)                //= [6]
ArrayUtils.remove([2, 6], 1)                //= [2]
ArrayUtils.remove([2, 6, 3], 1)             //= [2, 3]
说明删除数组中指定索引的值所有后续元素左移下标减一),此方法返回一个新数组该数组与新输入数组元素相同但是在指定位置上的元素除外返回数组的数据类型总是与输入数组的数据类型相同如果输入的数组是null,将会抛出IndexOutOfBoundsException 异常因为在这种情况下不能指定有效的索引
public static long[] remove(long[] array, int index)

// 17.删除数组中某个对象(从正序开始搜索,删除第一个)
ArrayUtils.removeElement(new int[] { 1, 3, 5 }, 3);// {1,5} 
ArrayUtils.removeElement(null, 1)                  //= null
ArrayUtils.removeElement([], 1)                    //= []
ArrayUtils.removeElement([1], 2)                   //= [1]
ArrayUtils.removeElement([1, 3], 1)                //= [3]
ArrayUtils.removeElement([1, 3, 1], 1)             //= [3, 1]
//说明:从指定的数组中移除指定的第一个元素,所有的后续元素左移(下标减一),如果数组中不包含这样的元素,不会从数组中移除元素,此方法返回输入数组中的所有元素移除掉指定元素,返回数组的数据类型总是与输入数组相同;
//返回值是一个新数组包含了现有数组的所有元素去除掉指定元素的第一次出现;
public static long[] removeElement(long[] array, long element)

字符串操作

String根据下标访问

1
2
String s = "abcd"
Char c = s.charAt(0)//c结果为a

String.format()的详细用法

问题

在开发的时候一段字符串的中间某一部分是需要可变的 比如一个Textview需要显示”XXX用户来自 上海 年龄 21 性别 男” 其中的 XXX 是用户名 每个用户也是不一样的 地区 上海 为可变的string数据 年龄 21 为可变的int数据 性别 男 为可变的string数据 遇到这种情况你们是怎么样解决的呢?把这段字符串保存在常量类里吗?不!我们应该遵循Google的开发模式

XML

1
<string name="user_info'> %1$s</span> 用户来自 <span class="hljs-variable">%2</span><span class="hljs-variable">$s  年龄 %3$d</span>  性别 <span class="hljs-variable">%4</span><span class="hljs-variable">$s</string>  

JAVA

1
2
3
4
5
6
String userName="XXX";
String userProvince="上海"; 
int userAge=21;
String userSex="男";
String string=getResources().getString(R.string.user_info);
String userInfo=String.format(string,userName,userProvince,userAge,userSex);

是不是觉得很方便 本来是打算当笔记记录下来备忘的,但是有朋友有朋友问到的一些相关的东西,我就完善一下吧

String.format()字符串常规类型格式化的两种重载方式

  • format(String format, Object… args) 新字符串使用本地语言环境,制定字符串格式和参数生成格式化的新字符串。
  • format(Locale locale, String format, Object… args) 使用指定的语言环境,制定字符串格式和参数生成格式化的字符串。

上个栗子有用到了字符类型和整数类型的格式化 下面我把常用的类型例举出来

转换符 详细说明 示例
%s 字符串类型 “喜欢请收藏”
%c 字符类型 ‘m’
%b 布尔类型 true
%d 整数类型(十进制) 88
%x 整数类型(十六进制) FF
%o 整数类型(八进制) 77
%f 浮点类型 8.888
%a 十六进制浮点类型 FF.35AE
%e 指数类型 9.38e+5
%g 通用浮点类型(f和e类型中较短的) 不举例(基本用不到)
%h 散列码 不举例(基本用不到)
%% 百分比类型 %(%特殊字符%%才能显示%)
%n 换行符 不举例(基本用不到)
%tx 日期与时间类型(x代表不同的日期与时间转换符) 不举例(基本用不到)

为了方便理解还是举个例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    String str=null;  
    str=String.format("Hi,%s", "小超");  
    System.out.println(str);  
    str=String.format("Hi,%s %s %s", "小超","是个","大帅哥");            
    System.out.println(str);                           
    System.out.printf("字母c的大写是:%c %n", 'C');  
    System.out.printf("布尔结果是:%b %n", "小超".equal("帅哥"));  
    System.out.printf("100的一半是:%d %n", 100/2);  
    System.out.printf("100的16进制数是:%x %n", 100);  
    System.out.printf("100的8进制数是:%o %n", 100);  
    System.out.printf("50元的书打8.5折扣是:%f 元%n", 50*0.85);  
    System.out.printf("上面价格的16进制数是:%a %n", 50*0.85);  
    System.out.printf("上面价格的指数表示:%e %n", 50*0.85);  
    System.out.printf("上面价格的指数和浮点数结果的长度较短的是:%g %n", 50*0.85);  
    System.out.printf("上面的折扣是%d%% %n", 85);  
    System.out.printf("字母A的散列码是:%h %n", 'A');  

输出结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Hi,小超 
Hi,小超 是个 大帅哥  
字母c的大写是:C   
布尔的结果是:false   
100的一半是:50   
100的16进制数是:64   
100的8进制数是:144   
50元的书打8.5折扣是:42.500000 元  
上面价格的16进制数是:0x1.54p5   
上面价格的指数表示:4.250000e+01   
上面价格的指数和浮点数结果的长度较短的是:42.5000   
上面的折扣是85%   
字母A的散列码是:41   

###搭配转换符还有实现高级功能 第一个例子中有用到 $

标志 说明 示例 结果
+ 为正数或者负数添加符号 (“%+d”,15) +15
0 数字前面补0(加密常用) (“%04d”, 99) 0099
空格 在整数之前添加指定数量的空格 (“% 4d”, 99) 99
, 以“,”对数字分组(常用显示金额) (“%,f”, 9999.99) 9,999.990000
( 使用括号包含负数 (“%(f”, -99.99) (99.990000)
# 如果是浮点数则包含小数点,如果是16进制或8进制则添加0x或0 (“%#x”, 99)(“%#o”, 99) 0x63 0143
< 格式化前一个转换符所描述的参数 (“%f和%<3.2f”, 99.45) 99.450000和99.45
d,%2$s”, 99,”abc”) 99,abc

第一个例子中有说到 %tx x代表日期转换符 我也顺便列举下日期转换符

标志 说明 示例
c 包括全部日期和时间信息 星期六 十月 27 14:21:20 CST 2007
F “年-月-日”格式 2007-10-27
D “月/日/年”格式 10/27/07
r “HH:MM:SS PM”格式(12时制) 02:25:51 下午
T “HH:MM:SS”格式(24时制) 14:28:16
R “HH:MM”格式(24时制) 14:28

来个例子方便理解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
	Date date=new Date();                                  
    //c的使用  
    System.out.printf("全部日期和时间信息:%tc%n",date);          
    //f的使用  
    System.out.printf("年-月-日格式:%tF%n",date);  
    //d的使用  
    System.out.printf("月/日/年格式:%tD%n",date);  
    //r的使用  
    System.out.printf("HH:MM:SS PM格式(12时制):%tr%n",date);  
    //t的使用  
    System.out.printf("HH:MM:SS格式(24时制):%tT%n",date);  
    //R的使用  
    System.out.printf("HH:MM格式(24时制):%tR",date);  

输出结果

1
2
3
4
5
6
全部日期和时间信息:星期三 九月 21 22:43:36 CST 2016  
年-月-日格式:2016-09-21
月/日/年格式:16/10/21  
HH:MM:SS PM格式(12时制):10:43:36 下午  
HH:MM:SS格式(24时制):22:43:36  
HH:MM格式(24时制):22:43  

其实还有很多其他有趣的玩法 我这边只列举一些常用的 有兴趣的朋友可以自己再去多了解了解

特殊情况

格式化变长度

1
2
3
4
5
6
7
8
9
int n = 5;
StringBuilder sb = new StringBuilder();
sb.append(String.format("第%d行:\n", n));
StringBuilder sb1 = new StringBuilder("%");
sb1.append(12 + String.valueOf(n).length() * 2);
sb1.append("s");
sb1.append("1111");
sb.append(String.format(sb1.toString(), " "));
System.out.println(sb);

关于foreach循环不能修改变量的值问题

一、发现问题

直接上代码

1
2
3
4
5
6
7
8
List<Integer>[]  lists = new ArrayList[5];
for (List list : lists){
	list = new ArrayList();
}

for (List list : lists){
	System.out.println(list);
}

观察这段简单的代码,首先创建了一个ArrayList的数组,然后通过foreach循环对该数组进行赋值,随后打印该数组。

预期将会打印出 5 行list的toString()信息,但是,实际情况却有所不一样。

运行结果:

1
2
3
4
5
6
7
null
null
null
null
null

Process finished with exit code 0

为什么会出现这样的情况?不是赋值了嘛?

既然这样,难道是我赋值的姿势不正确?要不我换一种方式,直接用for进行赋值

1
2
3
4
5
6
7
8
List<Integer>[]  lists = new ArrayList[5];
for (int i=0;i<5;i++){
	lists[i] = new ArrayList();
}

for (List list : lists){
	System.out.println(list);
}

运行结果如下:

1
2
3
4
5
6
7
[]
[]
[]
[]
[]

Process finished with exit code 0

这次成功赋值了,对比之下,我们大概可以猜到是因为使用了foreach而导致赋值失败的。

二、探究原因

首先得分析JDK源码,foreach基于容器或者数组的迭代器,也就是Iterator实现的,在翻阅源码的过程中,发现迭代器有一个方法forEach.

public void forEach(Consumer<? super E> action) { Objects.requireNonNull(action); int expectedModCount = this.modCount; Object[] es = this.elementData; int size = this.size;

1
2
3
4
5
6
7
8
for(int i = 0; this.modCount == expectedModCount && i < size; ++i) {
    	action.accept(elementAt(es, i));
    }

    if (this.modCount != expectedModCount) {
    	throw new ConcurrentModificationException();
    }
}

该方法正是实现foreach的关键,可以看出,增强型循环将数组或者容器传入方法体,方法体再执行操作,而这个过程就涉及到了 JAVA参数是值传递 的问题,如果不理解,可以参考这篇文章,有比较详细的介绍。

链接: 如何理解Java是值传递?.

也就是说,对foreach的操作其实是对数组或者容器的拷贝的操作。

./36.png

在这里插入图片描述 所以对原来数据的值(基本数据类型、引用)将不会发生改变,但可以修改引用的属性。

三、验证

下面将进行对foreach修改引用的属性的测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
List<Integer>[]  lists = new ArrayList[5];
for (int i=0;i<5;i++){
    lists[i] = new ArrayList();
}

for (List list : lists){
    list.add(1);
}

for (List list : lists){
    System.out.println(list);
}

首先创建数组,并且赋值,然后通过foreach修改数组的内容,最后打印

运行结果如下:

1
2
3
4
5
6
7
[1]
[1]
[1]
[1]
[1]

Process finished with exit code 0

验证成功!

四、总结

1.foreach的实现基于Iterator; 2.不能使用foreach对数组或容器进行赋值; 3.可以使用foreach修改数组或容器的对象的属性; 4.赋值行为尽量使用for循环。