1、集合
在Java中,如果一个Java对象可以在内部持有若干其他 Java 对象,并对外提供访问接口,我们把这种Java对象称为集合。很显然,Java 的数组可以看作是一种集合。
在Java中数组有如下限制:
- 数组初始化后大小不可变;
- 数组只能按索引顺序存取。
因此,我们需要各种不同类型的集合类来处理不同的数据,例如:
- 可变大小的顺序链表;
- 保证无重复元素的集合.....
最常用的集合有:ArrayList,HashSet,HashMap,Array(数组)。
Java标准库自带的java.util
包提供了集合类:Collection ,它是除 Map 外所有其他集合类的根接口。Java的java.util
包主要提供了以下三种类型的集合:
List
:一种有序列表的集合,例如,按索引排列的Student
的List
;Set
:一种保证没有重复元素的集合,例如,所有无重复名称的Student
的Set
;Map
:一种通过键值(key-value)查找的映射表集合,例如,根据Student
的name
查找对应Student
的Map
。
1.1、集合的特点
java 中的集合可以存放不同类型、不限数量的数据类型(可以通过泛型来限制只可以存入某种类型的数据)。Java 中的集合只能存放对象,比如你把一个 int 类型的数据 1 放入集合,其实它是自动转换成 Integer 类后放入的。
Java集合的其他特点:
- 实现了接口和实现类相分离,例如,有序表的接口是
List
,具体的实现类有ArrayList
,LinkedList
等, - 支持泛型,我们可以限制在一个集合中只能放入同一种数据类型的元素,例如:List<String> list = new ArrayList<>(); // 只能放入String类型
- Java访问集合总是通过统一的方式——迭代器(Iterator)来实现,通过迭代器访问无需知道集合内部的元素的存储方式
2、List 集合(有序可重复,可变长度数组)
List 集合是一种有序列表,它的行为和数组几乎完全相同:List 内部按照放入元素的先后顺序存放,每个元素都可以通过索引确定自己的位置,List
的索引和数组一样,从0
开始。List 集合的大小是可变的。
List 代表一个元素有序且可重复的集合,集合中的每个元素都有其对应的顺序索引。
List 允许使用重复元素,可以通过索引来访问指定位置的集合元素。List 默认按元素的添加顺序设置元素的索引
2.1、ArrayList 有序列表
在实际应用中,需要增删元素的有序列表,我们使用最多的是ArrayList。实际上,
ArrayList
在内部是使用数组来存储所有元素的。
2.2、LinkedList 有序列表
ArrayList 通过数组的方式实现了 List 接口,LinkedList
通过“链表”的方式实现了List接口,在LinkedList
中,它的内部每个元素都指向下一个元素:
2.3、ArryList 和 LinkedList 的对比
ArrayList 集合的底层数据结构是数组,它查询快,增删慢!线程不安全,但是效率高!
LinkedList 集合的底层数据结构是链表,它查询慢,但是增删快!线程不安全,但是效率高!
在各种Lists中,最好的做法是以 ArrayList 作为缺省选择。当插入、删除频繁时,可以使用 LinkedList()
2.4、List 集合的几个主要接口方法
List<E>
接口的几个主要的接口方法:
- 在末尾添加一个元素:
void add(E e)
- 在指定索引添加一个元素:
void add(int index, E e)
- 删除指定索引的元素:
int remove(int index)
- 删除某个元素:
int remove(Object e)
- 获取指定索引的元素:
E get(int index)
- 获取链表大小(包含元素的个数):
int size()
- 获取元素第一次出现的位置:int indexOf(E e)
- 修改指定位置的元素:E set(int index, E e )
示例:
List<String> list = new ArrayList<>(); list.add("apple"); list.add("null"); // 允许添加 null list.add("apple"); // 允许重复添加元素 System.out.println(list.size()); //size: 3 String str = list.get(0); // "apple"
创建 List:
除了使用ArrayList
和LinkedList
,我们还可以通过List
接口提供的of()
方法,根据给定元素快速创建List
:
List<Integer> list = List.of(1, 2, 5);
但是List.of()
方法不接受null
值,如果传入null
,会抛出NullPointerException
异常。
2.5、遍历 List 集合
2.5.1、for 循环遍历(不推荐使用)
和数组类型一样,我们要遍历一个List
,完全可以用for
循环根据索引配合get(int)
方法遍历:
List<String> list = List.of("apple", "pear", "banana"); for (int i=0; i<list.size(); i++) { String s = list.get(i); System.out.println(s); }
但这种方式并不推荐,一是代码复杂,二是因为get(int)
方法只有ArrayList
的实现是高效的,换成LinkedList
后,索引越大,访问速度越慢。
2.5.2、迭代器 Iterator 遍历(推荐使用)
for 循环使用迭代器
我们应该始终坚持使用迭代器
本身也是一个对象,但它是由Iterator
来访问List,
IteratorList
的实例调用iterator()
方法的时候创建的。Iterator
对象知道如何遍历一个List
,并且不同的List
类型,返回的Iterator
对象实现也是不同的,但总是具有最高的访问效率。
List<String> list = List.of("apple", "pear", "banana"); for (Iterator<String> it = list.iterator(); it.hasNext(); ) { String s = it.next(); System.out.println(s); }
for each 循环使用迭代器
通过Iterator
遍历List
永远是最高效的方式。由于Iterator
遍历非常常用,为了简洁代码,在 Java 中也支持通过 for each 循环来使用Iterator
遍历
public class Main { public static void main(String[] args) { List<String> list = List.of("apple", "pear", "banana"); for (String s : list) { System.out.println(s); } } }
实际上,只要实现了Iterable
接口的集合类都可以直接用for each
循环来遍历,Java编译器本身并不知道如何遍历集合对象,但它会自动把for each
循环变成Iterator
的调用,原因就在于Iterable
接口定义了一个Iterator<E> iterator()
方法,强迫集合类必须返回一个Iterator
实例。
2.6、编写equals()方法以满足contains()和indexOf()方法的使用
要正确使用List
的contains()
、indexOf()
方法,放入的实例必须正确覆写equals()
方法。
List
提供了boolean contains(Object o)
方法来判断List
是否包含某个指定元素。此外,int indexOf(Object o)
方法可以返回某个元素的索引,如果元素不存在,就返回-1
。
List<String> list = List.of("A", "B", "C"); System.out.println(list.contains("C")); // true System.out.println(list.contains("X")); // false System.out.println(list.indexOf("C")); // 2 System.out.println(list.indexOf("X")); // -1
List
内部并不是通过==
判断两个元素是否相等,而是使用equals()
方法判断两个元素是否相等。因此,要正确使用List
的contains()
、indexOf()
这些方法,放入的实例必须正确覆写equals()
方法,否则,放进去的实例,查找不到。我们之所以能正常放入String
、Integer
这些对象,是因为Java标准库定义的这些类已经正确实现了equals()
方法。
代码示例:
//下面的list集合中虽然放入了new Person("Bob"),但是用另一个new Person("Bob")查询不到,原因就是Person类没有覆写equals()方法。 public class Main { public static void main(String[] args) { List<Person> list = List.of( new Person("Bob") ); System.out.println(list.contains(new Person("Bob"))); // false } } class Person { String name; public Person(String name) { this.name = name; } }
2.6.1、如何覆写equals方法
equals()
方法的正确编写方法:
- 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等
- 用
instanceof
判断传入的待比较的Object
是不是当前类型,如果是,继续比较,否则,返回false
; - 对引用类型用
Objects.equals()
比较,对基本类型直接用==
比较。使用Objects.equals()
比较两个引用类型是否相等可以省去判断null
的麻烦。两个引用类型都是null
时它们也是相等的。
首先,我们要定义“相等”的逻辑含义。比如对于Person
类,如果name
相等,并且age
相等,我们就认为两个Person
实例相等。
public boolean equals(Object o) { if (o instanceof Person) { Person p = (Person) o; return Objects.equals(this.name, p.name) && this.age == p.age; } return false; }
如果不调用List
的contains()
、indexOf()
这些方法,那么放入的元素就不需要实现equals()
方法
3、Map 集合(key--value,类似于对象)
Map 集合是一种通过键值(key-value)查找的映射表集合,map 集合的作用就是能高效地通过 key 来查找对应的 value 值,提高查找效率。
Map 中不允许存在重复的 key,如果放入相同的key,只会把原有的 key-value 对应的 value 给替换掉。
和 List 类似,Map 也是一个接口,最常用的实现类是 HashMap。
3.1、关于Map集合的方法
put(k, v) 和 get(k) 方法存取元素
通过 put(key, value) 方法来放入值,如果 key 已经存在,该方法返回已经存在的 key 对应的 value 值,并且后放入的 value 将会替代掉旧的 value。如果 key 不存在,该方法返回 null。
通过 get(key) 方法来取值,如果 key 在 map 集合中不存在,将返回 null 。
public class Main { public static void main(String[] args) { Student s = new Student("Xiao Ming", 99); Map<String, Student> map = new HashMap<>(); map.put("Xiao Ming", s); // 插入值,将"Xiao Ming"和Student实例映射并关联 Student target = map.get("Xiao Ming"); // 取值,通过key查找并返回映射的Student实例 System.out.println(target == s); // true,同一个实例 Student another = map.get("Bob"); // 未找到返回 null } } class Student { public String name; public int score; public Student(String name, int score) { this.name = name; this.score = score; } }
Map 集合可以重复放入key-value
,但是一个key
只能关联一个value,后面放入的value会替代掉前面的value。如果放入的
key
已经存在,put()
方法会返回被删除的旧的value
,否则,返回null
。
在一个Map
中,虽然key
不能重复,但value
是可以重复的
查询key是否存在(containsKey(k)):如果只是想查询某个key
是否存在,可以调用boolean containsKey(K key)
方法。
查询value是否存在(containsValue(v))
根据key值移除元素(mapObj.remove(k))
3.2、遍历 Map
Map
和List
不同,Map
存储的是key-value
的映射关系,并且,它不保证顺序。在遍历的时候,遍历的顺序既不一定是put()
时放入的key
的顺序,也不一定是key
的排序顺序。遍历的时候,每个key
会保证被遍历一次且仅遍历一次,但顺序完全没有保证,甚至对于不同的JDK版本,相同的代码遍历的输出顺序都是不同的!
HashMap 内部的 key 是无序的,TreeMap 的 key 是有序的。
3.2.1、遍历所有的key(keySet())
要遍历 Map 集合的 key 可以使用 for each 循环来遍历 Map 实例的 keySet() 方法返回的 Set 集合,该 Set 集合包含不重复的key
的集合:
Map<String, Integer> map = new HashMap<>(); map.put("apple", 123); map.put("pear", 456); map.put("banana", 789); for (String key : map.keySet()) { //keySet()方法返回一个不包含重复key的set集合 Integer value = map.get(key); System.out.println(key + " = " + value); }
3.2.2、直接遍历key--value(entrySet())
同时遍历key
和value
可以使用for each
循环遍历Map
对象的entrySet()
集合,它包含每一个key-value
映射:
Map<String, Integer> map = new HashMap<>(); map.put("apple", 123); map.put("pear", 456); map.put("banana", 789); for (Map.Entry<String, Integer> entry : map.entrySet()) { String key = entry.getKey(); Integer value = entry.getValue(); System.out.println(key + " = " + value); }
3.3、覆写equals()和hashcode()方法
正确使用 Map 必须保证:
-
作为
key
的对象必须正确覆写equals()
方法,相等的两个key
实例调用 eauals() 必须返回 true,不同的key返回false
-
作为
key
的对象还必须正确覆写hashCode()
方法,且hashCode()
方法要严格遵循以下规范:如果两个对象相等,则两个对象的hashCode()
必须相等;如果两个对象不相等,则两个对象的hashCode()
不要相等。
我们经常使用
String
作为key
,因为String
已经正确覆写了equals()和hashCode()
方法。
3.3.1、覆写equals()方法
要想保证通过内容相同但不一定是同一个对象的 key 获取到同一个 value 值,必须得保证作为 key 的对象已经正确地覆写了 equals() 方法。
通过不是同一个对象,但是内容相同的 key 实例可以取到同一个 value 值,这是因为 Map 内部对 key 做比较是通过 equals 方法实现的,这一点和List
查找元素需要正确覆写equals()
是一样的,即正确使用Map
必须保证:作为key
的对象必须正确覆写equals()
方法。
我们经常使用String
作为key
,因为String
已经正确覆写了equals()
方法。但如果我们放入的key
是一个自己写的类,就必须保证正确覆写了equals()
方法。
3.3.2、覆写hashcode()方法
Map 集合是通过 key 来
计算出 value 存储的索引的,计算方式是调用了 key 对象的 hashcode() 方法,它返回一个 int 整数,HashMap 正是通过这个方法直接定位key
对应的value
的索引,继而直接返回value
。
在正确实现equals()
的基础上,我们还需要正确实现hashCode(),代码示例:
public class Person { String firstName; String lastName; int age; @Override int hashCode() { int h = 0; h = 31 * h + firstName.hashCode(); h = 31 * h + lastName.hashCode(); h = 31 * h + age; return h; } //为了避免firstName或lastName为null,代码抛NullPointerException异常,hashCode方法可以这么写 @Override int hashCode() { return Objects.hash(firstName, lastName, age); } }
equals()方法中
用到的用于比较的每一个字段,都必须在hashCode()
中用于计算;equals()
中没有使用到的字段,绝不可放在hashCode()
中计算。
3.4、TreeMap(元素有序)
HashMap 内部的 key 是无序的,还有一种Map
,它在内部会对Key进行排序,这种Map
就是SortedMap
。注意到SortedMap
是接口,它的实现类是TreeMap
。TreeMap
不使用equals()
和hashCode(),所以不需要覆写equals和hashCode方法。
SortedMap
保证遍历时以Key的顺序来进行排序。例如,放入的Key是"apple"
、"pear"
、"orange"
,遍历的顺序一定是"apple"
、"orange"
、"pear"
,因为String
默认按字母排序:
使用TreeMap
时,放入的Key必须实现Comparable
接口。String
、Integer
这些类已经实现了Comparable
接口,因此可以直接作为Key使用。
如果作为Key的class没有实现Comparable
接口,那么,必须在创建TreeMap
时传入Comparator即
指定一个自定义排序算法:
import java.util.*; public class Main { public static void main(String[] args) { Map<Person, Integer> map = new TreeMap<>(new Comparator<Person>() { //在创建 TreeMap 时传入一个排序算法 public int compare(Person p1, Person p2) { return p1.name.compareTo(p2.name); } }); map.put(new Person("Tom"), 1); map.put(new Person("Bob"), 2); map.put(new Person("Lily"), 3); for (Person key : map.keySet()) { System.out.println(key); // {Person: Bob}, {Person: Lily}, {Person: Tom} } System.out.println(map.get(new Person("Bob"))); // 2 } } class Person { public String name; Person(String name) { this.name = name; } public String toString() { return "{Person: " + name + "}"; } } //比较的值是数字 Map<Student, Integer> map = new TreeMap<>(new Comparator<Student>() { public int compare(Student p1, Student p2) { if (p1.score == p2.score) { return 0; } return p1.score > p2.score ? -1 : 1; //或者可以直接借助Integer.compare(int, int)也可以返回正确的比较结果,就不用写上面这么多判断了 } });
4、Set 集合(元素不重复)
Set
用于存储不重复的元素集合,它主要提供以下几个方法:
添加元素:boolean add(E e)
- 删除元素:
boolean remove(Object e)
- 判断是否包含元素:
boolean contains(Object e)
代码示例:
Set<String> set = new HashSet<>(); System.out.println(set.add("abc")); // true System.out.println(set.add("abc")); // false,添加失败,因为元素已存在,不可添加重复元素 System.out.println(set.contains("abc")); // true,元素存在 System.out.println(set.contains("xyz")); // false,元素不存在 System.out.println(set.remove("hello")); // false,删除失败,因为元素不存在 System.out.println(set.size()); // 1,一共1个元素
放入Set
的元素和Map
的key类似,都要正确实现 equals() 和 hashCode()方法,否则该元素无法正确地放入 Set。
最常用的Set
实现类是HashSet
,实际上,HashSet
仅仅是对HashMap
的一个简单封装。
继承关系:
4.1、HashSet
Set
接口并不保证有序,而SortedSet
接口则保证元素是有序的。HashSet 是无序的,它实现了Set
接口,但没有实现SortedSet
接口;
当向一个 HashSet 集合存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据 hashCode值决定该元素在 HashSet 中的存储位置。如果两个元素的 equals 方法返回 true,但它们的 hashCode() 返回值不相等,那这两个元素仍然可以添加成功,hashSet 会将它们存储在不同的位置。
HashSet 集合判断两个元素相等的标准:两个对象通过 equals() 方法比较返回 true,并且两个对象的 hashCode() 方法返回值也相等。
如果两个对象通过 equals() 方法返回 true,这两个对象的 hashCode 值也应该相同。
HashSet 是无序的,可以存放 null。
public class Main { public static void main(String[] args) { Set<String> set = new HashSet<>(); set.add("apple"); set.add("banana"); set.add("pear"); set.add("orange"); for (String s : set) { System.out.println(s); //乱序输出 banana apple pear ... } } }
4.2、TreeSet
TreeSet 是有序的,因为它实现了SortedSet
接口。在遍历TreeSet
时,输出就是有序的,顺序按插入的元素ASCll码排序。
public class Main { public static void main(String[] args) { Set<String> set = new TreeSet<>(); set.add("apple"); set.add("banana"); set.add("pear"); set.add("orange"); for (String s : set) { System.out.println(s); //输出apple banana orange pear } } }
使用TreeSet
和使用TreeMap
的要求一样,添加的元素必须正确实现Comparable
接口,如果没有实现Comparable
接口,那么创建TreeSet
时必须传入一个Comparator
对象。
5、迭代器的使用
迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。迭代器通常被称为“轻量级”对象,因为创建它的代价小。
Java中的Iterator功能比较简单,并且只能单向移动:
- 使用 iterator() 方法会返回一个 Iterator 对象。第一次调用 Iterator 对象的 next() 方法时,它返回序列的第一个元素。(注意,iterator() 方法是 java.lang.Iterable接口,被 Collection 继承)
- 使用 next() 获得序列中的下一个元素
- 使用 hasNext() 检查序列中是否还有元素
- 使用 remove() 将迭代器新返回的元素删除
Iterator 是Java迭代器最简单的实现,为 List 设计的 ListIterator 具有更多的功能,它可以从两个方向遍历List,也可以从 List 中插入和删除元素。
list l = new ArrayList(); l.add("aa"); l.add("bb"); for (Iterator iter = l.iterator(); iter.hasNext();) { String str = (String)iter.next(); System.out.println(str); } /*迭代器用于 while 循环遍历 Iterator iter = l.iterator(); while(iter.hasNext()){ String str = (String) iter.next(); System.out.println(str); } */ /*迭代器用于 for each 循环遍历 for(Object obj : iter) { System.out.println(obj); } */