核心类库 - 集合
1 类集
类集设置的目的:
类集是Java对数据结构成熟的实现。数组在使用时,数组的元素个数是固定的,虽然我们可以通过链表、二叉树等各种各样的数据结构来完成能无限存储数据还不需要动态扩容的机制,但是依然很麻烦,每次想存东西都得自己写一个数据结构来实现。Java中内置了一些数据结构的实现,可以把类集理解为Java对各种数据结构的实现,它包含了各种各样的常用数据结构,每种数据结构都有它擅长的点。
Java类集结构图:![类集](https://i-blog.csdnimg.cn/blog_migrate/12c133f47eda96ff991fc0b3c809c784.png)
Collection指的是一些进行单值存储的集合的顶级接口;Map是一些进行双值存储(键值对)的集合的顶级接口;Iterator是迭代器,对于所有集合进行了迭代的实现,使用迭代器获取集合中的数据都是对这个数据结构来说最优的获取方式,所有类集操作的接口或类都在java.util包中。
2 Collection接口
Collection接口是在整个Java类集中单值存储的最大操作父接口,里面每次操作的时候都只能保存一个对象的数据。
常用方法:
1.add(E e)
向集合中插入一个元素,成功返回true,失败返回false。
2.addAll(Collection<? extends E> c)
向集合中插入一组元素,成功返回true,失败返回false。
3.clear()
清空集合中的元素。
4.contains(Object o)
判断一个元素是否在当前集合中,在则返回true,否则返回false。
5.containsAll(Collection<?> c)
判断一个集合是否在当前集合中,在则返回true,否则返回false。
6.isEmpty()
判断集合是否为空,是则返回true,否则返回false。
7.remove(Object o)
从集合中删除一个对象,成功返回true,失败返回false。
8.removeAll(Collection<?> c)
从集合中删除一组对象,成功返回true,失败返回false。
9.retainAll(Collection<?> c)
判断一个集合是否不在当前集合中,不在返回true,否则返回false。
10.size()
获取集合中元素的个数(长度),类型为Int。
11.toArray()
把当前集合转换为Object数组。
12.toArray(T[] a)
把当前集合转换为指定类型的数组。
13.equals(Object o)
从Object类中重写而来,判断指定对象和当前集合是否相等。
14.hashCode()
从Object类中重写而来,返回此集合的哈希值,类型为int。
但是,这些方法都是抽象的,并且因为Collection接口是整个类集单值的最大父接口,所以通常不使用Collection接口,而是使用它的两个子接口:List、Set(它们都继承了Collection的这些方法),其中List中所存的元素是允许重复的,而Set中所存的元素是不允许重复的,它们都是单值存储。
3 List接口及其实现类
3.1 List接口
List是Collection的子接口,所存的元素是允许重复的,它除了继承Collection的方法外,还有以下常用方法。
常用方法:
1.add(int index,E element)
在指定位置处插入一个元素。
2.addAll(int index,Collection<? extends E> c)
在指定位置处插入一组元素。
3.get(int index)
获取集合中指定下标位置的数据。
4.indexOf(Object o)
返回集合中第一次出现指定对象的下标位置,不存在则返回-1。
5.lastIndexOf(Object o)
返回集合中最后一次出现指定对象的下标位置,不存在则返回-1。
6.remove(int index)
删除集合中指定下标位置的数据,并返回此数据。该方法和Collection接口中的remove方法构成方法的重载而非重写,因为传入的参数类型不同,两种方法均可使用。
7.set(int index,E element)
修改集合中指定下标位置的数据为传入的数据。
8.subList(int fromIndex,int toIndex)
返回集合从fromIndex下标开始到toIndex下标结束的子集合。
List底下有很多的实现类,常用的是ArrayList(动态数组的实现)和LinkedList(双向链表结构,可以模拟栈和队列),还有个Vector可以认为是ArrayList的早期实现。
3.2 ArrayList类
采用了异步处理,线程不安全,但是效率很高。使用的是数组结构,对于增加删除慢,查找快。
构造方法:
1.ArrayList()
创建一个初始长度为10的空数组,动态扩容,每次扩容是原大小的1.5倍。在这里初始长度10并不是开始时就给定的,而是首次扩容给定的。
2.ArrayList(int initialCapacity)
创建一个指定初始长度的空数组。
3.ArrayList(Collection<? extends E> c)
可以传入一个Collection对象(List和Set底下所有的集合都算),将其转换为ArrayList数组。
public static void main(String[] args) {
ArrayList<Integer> data = new ArrayList<>();
data.add(100);
data.add(200);
data.add(300);
data.add(200);
data.add(100);
System.out.println(data.get(2)); //300
System.out.println(data.indexOf(100)); //0
System.out.println(data.indexOf(500)); //-1
System.out.println(data.lastIndexOf(100)); //4
System.out.println(data.lastIndexOf(500)); //-1
System.out.println(data.remove(2)); //300
for (int i=0;i<data.size();i++) {
System.out.print(data.get(i)+"、"); //100、200、200、100、
}
}
3.3 Vector类
和ArrayList类似,但Vector采用的是同步处理,线程安全,但是效率较低。
构造方法:
1.Vector()
创建一个初始长度为10且每次扩容增量为0的空数组,增量为0则每次扩容是原大小的2倍。
2.Vector(int initialCapacity)
创建一个指定初始长度且每次扩容增量为0的空数组。
3.Vector(int initialCapacity, int capacityIncrement)
创建一个指定初始长度和指定每次扩容增量的空数组。
4.Vector(Collection<? extends E> c)
可以传入一个Collection对象(List和Set底下所有的集合都算),将其转换为Vector数组。
3.4 LinkedList类
使用的是双向链表结构,对于增加删除快,查找慢,它除了继承List的方法外,还有以下常用方法。
常用方法:
1.addFirst(E e)
向链表的首部添加元素。
2.addLast(E e)
向链表的尾部添加元素。
3.getFirst()
返回此链表的第一个元素。
4.getLast()
返回此链表的最后一个元素。
5.push(E e)
压栈,向链表的首部添加元素。
6.pop()
弹栈,返回此链表的第一个元素。
7.removeFirst()
从链表中删除并返回第一个元素。
8.removeLast()
从链表中删除并返回最后一个元素。
public static void main(String[] args) {
LinkedList<Integer> data = new LinkedList<>();
data.addFirst(100);
data.addFirst(200);
System.out.println(data.removeFirst()); //200
System.out.println(data.removeLast()); //100
data.push(100);
data.push(200);
System.out.println(data.pop()); //200
}
4 集合输出
4.1 Iterator迭代器
用于遍历集合,从集合中取数据。Iterator迭代器可以迭代Collection下的所有集合(List、Set),而ListIterator迭代器只能用于迭代List下的集合。
常用方法:
1.hasNext()
判断集合是否有下一个元素,有则返回true,否则返回false。
2.next()
返回集合中的下一个元素并让指针指向此元素。注意:指针默认不指向任何元素,即默认指向第1个元素前面的位置。
3.remove()
删除指针所指向的元素数据。由于指针默认不指向任何元素,因此必须通过next把指针移动到指定元素位置才能删除数据。
public static void main(String[] args) {
ArrayList<Integer> data = new ArrayList<>();
data.add(1);
data.add(2);
data.add(3);
data.add(4);
data.add(5);
Iterator<Integer> iterator = data.iterator();
iterator.next(); //指针指向第1个数据
iterator.remove(); //删除指针所指向的数据
System.out.println(data.size()); //4
while (iterator.hasNext()) {
System.out.println(iterator.next()); //2 3 4 5
}
}
4.2 ListIterator迭代器
是Iterator的子接口,只能用于迭代List下的集合,它除了继承Iterator的方法外,还有以下常用方法。
常用方法:
1.add(E e)
向集合中插入元素,在指针所指向的元素之前插入。
2.hasPrevious()
判断集合是否有上一个元素,有则返回true,否则返回false。
3.previous()
返回集合中的上一个元素并让指针指向此元素。
4.set(E e)
修改指针所指向的元素数据。
5.previousIndex()
返回指针所指向的元素的上一个元素的索引位置。
6.nextIndex()
返回指针所指向的元素的下一个元素的索引位置。
public static void main(String[] args) {
ArrayList<Integer> data = new ArrayList<>();
data.add(1);
data.add(2);
data.add(3);
data.add(4);
data.add(5);
ListIterator<Integer> iterator = data.listIterator(); //目前的集合:[X] 1 2 3 4 5,指针指向初始位置
iterator.add(100); //目前的集合:100 [X] 1 2 3 4 5,指针指向初始位置
System.out.println(data.size()); //6
iterator.next(); //目前的集合:100 X [1] 2 3 4 5,指针指向1
iterator.next(); //目前的集合:100 X 1 [2] 3 4 5,指针指向2
iterator.set(200); //目前的集合:100 X 1 [200] 3 4 5,指针所指向的2被改为200
iterator.previous(); //目前的集合:100 X [1] 200 3 4 5,指针指向1
iterator.previous(); //目前的集合:100 [X] 1 200 3 4 5,指针指向初始位置
iterator.previous(); //目前的集合:[100] X 1 200 3 4 5,指针指向100
while (iterator.hasNext()) {
System.out.println(iterator.next()); //100 1 200 3 4 5
}
}
4.3 forEach
即增强for循环,最早出现在C#中,用于迭代数组或Collection下的集合,语法如下:
for(数据类型 变量名:集合或数组名) {
}
数据类型是要遍历的集合或数组里面的数据类型,变量名在循环过程中会一直变化为它所指向的数据,变化范围是从集合或数组的开始到最后。
public static void main(String[] args) {
int[] arr = {6,5,4,3,2,1};
for (int data:arr) {
System.out.println(data); //6 5 4 3 2 1
}
ArrayList<String> data = new ArrayList<>();
data.add("AAA");
data.add("BBB");
data.add("CCC");
data.add("DDD");
for (String s:data) {
System.out.println(s); //AAA BBB CCC DDD
}
}
5 Set接口及其实现类
5.1 Set接口
Set也是Collection的子接口,所存的元素是不允许重复的,如果将可变对象用作Set对象,则必须非常小心,因为Set的某些子类是无序存储的,有的时候由于属性更改可能会导致一些问题发生。Set接口并没有扩展太多内容,所以使用方式和Collection提供的方式差不多,它没有get方法,可以使用toArray将其转换成数组再去遍历寻找,也可以使用Iterator对其进行迭代。
5.2 HashSet类
HashSet内部使用了哈希表的存储方式,是一种散列存放的数据结构,不保证存储顺序。单值存储HashSet内部实际上是利用了双值存储HashMap,其中一个值即为传入的元素,另一个值是指定好的常量(固定的new Object()),关于哈希表和双值存储HashMap的内容,见6.2 HashMap类。
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
System.out.println(set.add("AAA")); //true
System.out.println(set.add("BBB")); //true
System.out.println(set.add("CCC")); //true
System.out.println(set.add("BBB")); //false,Set集合元素不能重复
System.out.println(set.add("AAA")); //false,Set集合元素不能重复
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next()); //AAA CCC BBB,HashSet不保证存储顺序
}
}
5.3 TreeSet类
TreeSet是采用有序的二叉树来存储的,默认情况下是自然顺序。和HashSet类似,TreeSet是基于Map下的TreeMap集合实现的,也是单值存储。TreeSet的Iterator迭代器是快速失败的,快速失败即迭代器遍历的是集合本身,如果迭代过程中集合发生了变化,则会发生异常;而安全失败指的是这个失败不会出错,在遍历前会先将集合复制一份,迭代器遍历的是复制的集合,即使迭代过程中集合发生了变化,也不会发生异常,有些集合的迭代器是快速失败的,而有些集合的迭代器是安全失败的,通常情况下如果不特殊描述指的都是安全失败。
TreeSet所谓的存储有序并不是根据存储的顺序,而是根据数据的顺序进行排序,在Java中使用的是基于unicode的编码(当前也是基于ASCII码的),按照编码进行排序。
public static void main(String[] args) {
TreeSet<String> data = new TreeSet<>();
data.add("B");
data.add("C");
data.add("A");
data.add("D");
for (String s:data) {
System.out.println(s); //A B C D
}
}
所以虽然输入的是B、C、A、D,但是根据编码顺序,存储顺序会变成A、B、C、D。如果存储的是系统所提供的数据,是可以正常有序存储的,但是如果存储的是自定义的对象,就会出现问题,因为系统并不知道该如何帮你排序,所以谁在前谁在后应该由定义数据的人来指定,如果不指定,就会产生异常,例如:
public static void main(String[] args) {
TreeSet<Person> data = new TreeSet<>();
Person p1 = new Person("张三",18);
Person p2 = new Person("李四",19);
data.add(p1); //出现异常
data.add(p2);
}
static class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
可以看到,在第一次添加数据的时候就已经出现了ClassCastException类型转换异常,在将Peson类转换成Comparable时出错。Comparable是什么?Comparable是一个接口,它有一个抽象方法,如果希望自己定义的类的对象能比较大小,就需要实现Comparable接口的compareTo方法。当需要比较大小时,通过调用对象的compareTo方法,把另外一个要比较的对象传进来,事实上就是拿this和传入的对象比较,其规则需要自己制定,返回正数代表this大,返回负数代表this小,返回0代表一样大,此时系统就会按照此规则进行比较。
public static void main(String[] args) {
TreeSet<Person> data = new TreeSet<>();
Person p1 = new Person("张三",19);
Person p2 = new Person("李四",18);
Person p3 = new Person("王五",19);
System.out.println(data.add(p1)); //true
System.out.println(data.add(p2)); //true
System.out.println(data.add(p3)); //false,Set集合元素不能重复
for(Person p:data) {
System.out.println(p); //name=李四,age=18 name=张三,age=19
}
}
static class Person implements Comparable<Person> {
public int compareTo(Person o) {
if(this.age > o.age) return 1;
else if(this.age == o.age) return 0;
return -1;
}
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return "name="+name+",age="+age;
}
}
当系统按照你制定的规则比较时,发现返回的是0,即两个数据一样大,由于Set集合不能存储一样的数据,当发现两个数据一样大时,后者就不会被存储,Set集合就是通过这种方式来实现不存储相同数据的。另外,除了让需要比较的类实现Comparable接口的compareTo方法外,还可以额外定义一个类实现Comparator接口的compare方法,即不改变需要比较的类,而是定义一个新的只用于比较的类。
public static void main(String[] args) {
Comparator<Person> comp = new Comp(); //创建比较器
TreeSet<Person> data = new TreeSet<>(comp); //在创建TreeSet时,传入比较器
Person p1 = new Person("张三",19);
Person p2 = new Person("李四",18);
Person p3 = new Person("王五",19);
System.out.println(data.add(p1)); //true
System.out.println(data.add(p2)); //true
System.out.println(data.add(p3)); //false,Set集合元素不能重复
for(Person p:data) {
System.out.println(p); //name=李四,age=18 name=张三,age=19
}
}
static class Comp implements Comparator<Person> { //比较器
public int compare(Person person1, Person person2) {
if(person1.age > person2.age) return 1;
else if(person1.age == person2.age) return 0;
return -1;
}
}
static class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return "name="+name+",age="+age;
}
}
Comparator接口的compare方法的实现和Comparable接口的compareTo方法的实现类似,只是compare方法是传入两个对象比较,而compareTo方法是当前对象和传入的另一个对象比较。Comparable比较器更常用一些,不过当需要通过多种不同的方式进行排序时,Comparator比较器是更好的选择。
6 Map接口及其实现类
6.1 Map接口
Map是一些进行双值存储的集合的顶级接口,和Collection接口是同一级别的,Map集合存储的是一个个的键值对数据,键(key)不可重复,每个键最多可以映射一个值,通过相应的键即可找到对应的值。大多数Set集合的内部都使用了Map集合键的位置来存储数据,而值的位置放了一个统一的数据,从而实现了自己内容不可重复的操作。
常用方法:
1.clear()
清空Map集合中的内容。
2.containsKey(Object key)
判断Map集合中是否存在指定键,存在返回true,不存在返回false。
3.containsValue(Object value)
判断Map集合中是否存在指定值,存在返回true,不存在返回false。
4.get(Object key)
根据键找到其对应的值并返回,不存在返回null。
5.isEmpty()
判断Map集合是否为空,是则返回true,否则返回false。
6.keySet()
把Map集合中的所有键单独作为一个Set集合返回。
7.values()
把Map集合中的所有值单独作为一个Collection集合返回。
8.put(K key, V value)
在Map集合中存储一个键和一个值,若存储时键已经有值,则用新值把旧值替换掉,并返回这个旧值,若无旧值则返回null。
9.putAll(Map<? extends K,? extends V> m)
在Map集合中存储一组键和一组值。
10.remove(Object key)
在Map集合中找到传入的键,并删除这个键值对,返回这个键之前的值,无则返回null。
11.remove(Object key, Object value)
在Map集合中找到传入的键和值,并删除这个键值对,键和值必须完全匹配时才会删除,删除成功返回true,否则返回false。
12.replace(K key, V value)
在Map集合中找到传入的键对应的值,用新值把旧值替换掉,并返回这个旧值,若无旧值则返回null。
13.replace(K key, V oldValue, V newValue)
在Map集合中找到传入的键和值,用新值把旧值替换掉,键和值必须完全匹配时才会替换,替换成功返回true,否则返回false。
14.size()
返回Map集合中键值对的数量,一对算1个,类型为int。
6.2 HashMap类
采用了异步处理,线程不安全,但是效率很高。是基于哈希表(散列表)的Map接口的实现,它的数据结构是哈希表,Java中对于HashMap的哈希表实现是采用对象数组加链表,在链表长度达到一定程度时,链表还会转换为二叉树。所有类都有一个hashCode方法,可以得到对象的哈希值(int类型),重写hashCode方法可以对哈希表的性能进行优化, 建议每一个自己创建的类都去实现hashCode的计算,按照自己制定的规则根据对象属性的不同使得计算出的哈希值能够均匀分布。例如:假设有10个对象,这10个对象的哈希值应该均匀分布在1-10之间,而不是互相抱团。关于哈希值的计算原则还有一点,如果两个对象属性完全相同,哈希值也不建议相同。
哈希表的结构是对象数组加链表,其中对象数组的默认长度是16,下标是0-15,哈希表在存储时,会先调用对象的hashCode方法取得哈希值,然后跟对象数组的长度进行取余的运算(%),得到一个0-15的余数,然后将对象存到数组的该下标中,这样一来,当查找一个数据,不需要对整个数组进行遍历,只需要得到它的哈希值,进行简单的取余运算,就能确定它在数组的哪个下标里。每个数组的下标中都有一个链表,当多个对象的哈希值相同时,先判断链表根部是否有数据,若没有则直接存在链表的根部,若有数据则往它的下一个去存,若下一个也有则往它的下一个的下一个去存,这样一直存储下去。我们把数组的每一个下标称作一个哈希桶,当哈希桶中的数据量大于8时,从链表转换为红黑二叉树,当哈希桶中的数据量减少到6时,从红黑二叉树转换为链表(面试题:哈希桶中已知数据是7个,减少到6个时,一定会从红黑二叉树转换为链表嘛?答:不一定,如果它曾经没有从链表转换为红黑二叉树,就不会从红黑二叉树转换为链表)。
哈希桶的大小是会变化的,其初始桶数量默认是16,散列因子默认是0.75,当哈希桶中存上数据的桶数量除以桶的总数量超过散列因子,那就会对桶进行扩容,每次扩容是原大小的2倍,此时由于数组的长度发生变化,内部的数据结构会重建,所有对象的哈希值都要重新和数组的长度进行取余的运算,以确定新的存储位置。因此为了避免频繁的散列操作,浪费系统性能,初始容量一定要给的合理一些。
构造方法:
1.HashMap()
使用默认初始容量16和默认散列因子0.75创建空HashMap。
2.HashMap(int initialCapacity)
使用指定初始容量和默认散列因子0.75创建空HashMap。
3.HashMap(int initialCapacity, float loadFactor)
使用指定初始容量和指定散列因子(0.75是官方测试后的最佳值;过大会导致大量的哈希值碰撞,即每个桶存了很多数据却仍然没有扩容,虽然哈希桶利用的好,但是效率低;过小则哈希桶很多空间被浪费,但是效率高)创建空HashMap。
4.HashMap(Map<? extends K,? extends V> m)
可以传入一个Map对象,将其转换为HashMap。
public static void main(String[] args) {
HashMap<String,String> data = new HashMap<>();
data.put("key1","AAA");
data.put("key2","BBB");
System.out.println(data.get("key1")); //AAA
System.out.println(data.get("key2")); //BBB
Set<String> set = data.keySet();
for(String key:set) {
System.out.println(key+"—>"+data.get(key)); //key1—>AAA key2—>BBB
}
Collection<String> values = data.values();
for(String value:values) {
System.out.println(value); //AAA BBB
}
}
6.3 Hashtable类
和HashMap基本一致,但Hashtable采用的是同步处理,若当前线程正在操作数组的某一个下标,那么即使另外一个线程要操作数组的其它下标,也必须等待当前线程操作结束,线程安全,但是效率较低。
6.4 ConcurrentHashMap类
和HashMap基本一致,但是ConcurrentHashMap采用的是分段锁机制,若当前线程正在操作数组的某一个下标,另外一个线程要操作数组的其它下标,那么可以直接进行操作,若另外一个线程要操作数组的同一个下标,则需要等待当前线程操作结束,保证线程安全的同时效率又不那么低。
6.5 TreeMap类
使用方法和HashMap类似,内部原理和TreeSet类似,只是变成了双值存储而已。
6.6 LinkedHashMap类
和HashMap类似,其实就是把数据存到HashMap的同时再存到一个双向链表中,拥有HashMap高性能的同时还能保证存储顺序。
6.7 自定义对象
当需要使用Map集合特别是HashMap这种哈希表的数据结构存储自定义对象时,一定要支持equals和hashCode,重写hashCode让哈希值根据当前对象的属性来计算,否则哈希值会根据Object提供的hashCode方法来计算,可能会导致计算的值分布不均匀,影响HashMap的性能。当Map集合的键值对已经存储时,不要随意修改其中的键,否则会导致一些问题。
public static void main(String[] args) {
HashMap<Book,String> data = new HashMap<>();
Book book1 = new Book("BOOK1","INFO1");
data.put(book1,"ABCDEFG");
Book book2 = new Book("BOOK2","INFO2");
data.put(book2,"HIJKLMN");
System.out.println(data.get(book1)); //ABCDEFG
book1.setName("BOOK3");
System.out.println(data.get(book1)); //null
}
static class Book {
private String name;
private String info;
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equals(name, book.name) &&
Objects.equals(info, book.info);
}
public int hashCode() {
return Objects.hash(name, info);
}
public Book(String name, String info) {
this.name = name;
this.info = info;
}
public void setName(String name) {
this.name = name;
}
}
在此例中,hashCode是由name和info共同计算的,存储到HashMap时以图书对象作为键,当使用get方法获取键对应的值时没有任何问题,但是如果改变了图书的名称,而图书对象没有变,再次使用get方法获取键对应的值就无法找到了,这是为什么呢?原因是查找时是根据图书对象的哈希值去找对应的哈希桶里面的数据,但是由于图书名称的改变导致哈希值最终计算的结果发生改变,所以就找不到原来的数据了。即使再创建一个新的对象和修改前的对象完全一致,根据这个新对象来取其中的值,也是无法找到的,因为虽然哈希值正确了,但是在正确的哈希桶中查找时,发现对象的属性被修改了,从而导致eqauls不满足,不认为是同一个键,返回值依然是null。
由此可见,当把自定义对象作为键时,存储这个自定义对象是使用该对象的hashCode方法计算哈希值并存储到对应的哈希桶中;查找这个键的对象是先使用该对象的hashCode方法计算哈希值,然后到对应的哈希桶中去和每一个键的对象使用equals方法,若为true,则说明找到了,这时才会返回这个键对应的值,所以一定不要忘记支持equals和hashCode。
因此,当对象作为哈希表的键存储时(作为计算哈希值的数据存储时),存储完毕后就不要修改了,如果一定要修改,就不要把此对象存在键的位置。
7 JDK9集合新特性
快速创建固定长度存储少量数据的集合的更便捷的方法,这些操作存在于List、Set、Map三个接口中。
public static void main(String[] args) {
List<String> list = List.of("AAA", "BBB", "CCC", "DDD");
for (String s:list) {
System.out.println(s); //AAA BBB CCC DDD
}
Set<String> set = Set.of("AAA", "BBB", "CCC", "DDD");
for (String s:set) {
System.out.println(s); //BBB AAA DDD CCC
}
Map<String,String> map = Map.of("AAA", "BBB", "CCC", "DDD");
Set<String> keySet = map.keySet();
for(String key:keySet) {
System.out.println(key+"—>"+map.get(key)); //AAA—>BBB CCC—>DDD
}
}