Java的集合类定义在java.util
包中,支持泛型,主要提供了3种集合类,包括List
,Set
和Map
。Java集合使用统一的Iterator
遍历。
1、List遍历
实现了Iterator
接口的集合类都可以直接用for each
循环来遍历,Java编译器本身并不知道如何遍历集合对象,但它会自动把for each
循环变成Iterator
的调用
public class Main {
public static void main(String[] args) {
List<String> list = List.of("AA", "BB", "CC");
for (String s : list) {
System.out.println(s);
}
}
}
2、List和Array转换
把List
变为Array
有三种方法
第一种是调用toArray()
方法直接返回一个Object[]
数组 Object[] array = list.toArray();
第二种方式是给toArray(T[])
传入一个类型相同的Array
,List
内部自动把元素复制到传入的Array
中 Integer[] array = list.toArray(new Integer[3]);
第三种方式是通过List
接口定义的T[] toArray(IntFunction<T[]> generator)
方法 Integer[] array = list.toArray(Integer[]::new);
3、List
是一种有序链表:List
内部按照放入元素的先后顺序存放,并且每个元素都可以通过索引确定自己的位置
List
提供了boolean contains(Object o)
方法来判断List
是否包含某个指定元素。此外,int indexOf(Object o)
方法可以返回某个元素的索引,如果元素不存在,就返回-1
。
往List
中添加的"C"
和调用contains("C")
传入的"C"
不是同一个实例,但是取到了元素和索引,因为List
内部并不是通过==判断两个元素是否相等,而是使用equals()
方法判断两个元素是否相等
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
list.add("C");
System.out.println(list.contains("B")); // true
System.out.println(list.indexOf("B")); // 1
System.out.println(list.contains(new String("C"))); // true
System.out.println(list.indexOf(new String("C"))); // 2
}
要正确使用List
的contains()
、indexOf()
这些方法,放入的实例必须正确覆写equals()
方法,否则,放进去的实例,查找不到。我们之所以能正常放入String
、Integer
这些对象,是因为Java标准库定义的这些类已经正确实现了equals()
方法。
equals()
方法的正确编写方法:
- 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
- 用
instanceof
判断传入的待比较的Object
是不是当前类型,如果是,继续比较,否则,返回false
; - 对引用类型用
Objects.equals()
比较,对基本类型直接用==
比较。
使用Objects.equals()
比较两个引用类型是否相等的目的是省去了判断null
的麻烦。两个引用类型都是null
时它们也是相等的。
如果不调用List
的contains()
、indexOf()
这些方法,那么放入的元素就不需要实现equals()
方法。
public class TestList {
public static void main(String[] args) {
List<Person> list = new ArrayList<Person>();
list.add(new Person("小王", "男", 18));
list.add(new Person("小明", "男", 25));
list.add(new Person("小李", "女", 20));
boolean exist = list.contains(new Person("小明", "男", 25));
System.out.println(exist ? "查询成功!" : "查询失败!");//查询成功;没有重写equals方法结果返回:查询失败
}
}
class Person {
String name;
String sex;
int age;
public Person(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
public boolean equals(Object o){
if(o instanceof Person){
Person p = (Person) o;
return this.name.equals(p.name) && Objects.equals(this.sex,p.sex) && this.age==p.age;
}
return false;
}
}
4、Map
这种键值(key-value)映射表的数据结构,作用就是能高效通过key
快速查找value
(元素)。
如果只是想查询某个key
是否存在,可以调用boolean containsKey(K key)
方法。
Map中不存在重复的key,因为放入相同的key,只会把原有的key-value对应的value给替换掉。
对Map
来说,要遍历key
可以使用for each
循环遍历Map
实例的keySet()
方法返回的Set
集合,它包含不重复的key
的集合。
同时遍历key
和value
可以使用for each
循环遍历Map
对象的entrySet()
集合,它包含每一个key-value
映射。
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<String, Integer>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
for(String key : map.keySet()){
Integer num = map.get(key);
System.out.println(num);// 1 2 3
}
for(Map.Entry<String,Integer> en :map.entrySet()){
String aa = en.getKey();
Integer bb = en.getValue();
System.out.println(aa+": "+bb);//A:1 B:2 C:3
}
}
5、编写equals和hashCode
HashMap
之所以能根据key
直接拿到value
,原因是它内部通过空间换时间的方法,用一个大数组存储所有value
,并根据key直接计算出value
应该存储在哪个索引
正确使用Map
必须保证:
-
作为
key
的对象必须正确覆写equals()
方法,相等的两个key
实例调用equals()
必须返回true
; -
作为
key
的对象还必须正确覆写hashCode()
方法,且hashCode()
方法要严格遵循以下规范:
- 如果两个对象相等,则两个对象的
hashCode()
必须相等; - 如果两个对象不相等,则两个对象的
hashCode()
尽量不要相等。
HashMap
初始化时默认的数组大小只有16
添加超过一定数量的key-value
时,HashMap
会在内部自动扩容,每次扩容一倍,即长度为16的数组扩展为长度32,相应地,需要重新确定hashCode()
计算的索引位置
HashMap
内部的数组长度总是2的n次方
不同的key
具有相同的hashCode()
的情况称之为哈希冲突
要正确使用HashMap
,作为key
的类必须正确覆写equals()
和hashCode()
方法;
一个类如果覆写了equals()
,就必须覆写hashCode()
,并且覆写规则是:
如果equals()
返回true
,则hashCode()
返回值必须相等;
如果equals()
返回false
,则hashCode()
返回值尽量不要相等。
6、TreeMap
还有一种Map
,它在内部会对Key进行排序,这种Map
就是SortedMap
。注意到SortedMap
是接口,它的实现类是TreeMap。
使用TreeMap
时,放入的Key必须实现Comparable
接口。String
、Integer
这些类已经实现了Comparable
接口,因此可以直接作为Key使用。作为Value的对象则没有任何要求。
public static void main(String[] args) {
//SortedMap内部会对Key进行排序。注意到SortedMap是接口,它的实现类是TreeMap。
Map<String, String> map = new TreeMap<String, String>();
map.put("aa", "11");
map.put("bb", "22");
map.put("cc", "33");
map.put("dd", "44");
//打印结果: 11 22 33 44
for(String name : map.keySet()){
String value = map.get(name);
System.out.print(value+" ");
}
}
7、Set
最常用的Set
实现类是HashSet
,实际上,HashSet
仅仅是对HashMap
的一个简单封装;经常用Set
用于去除重复元素
Set
接口并不保证有序,而SortedSet
接口则保证元素是有序的:
HashSet
是无序的,因为它实现了Set
接口,并没有实现SortedSet
接口;TreeSet
是有序的,因为它实现了SortedSet
接口。
8、Queue
队列(Queue
)是一种经常使用的集合。Queue
实际上是实现了一个先进先出(FIFO:First In First Out)的有序表。它和List
的区别在于,List
可以在任意位置添加和删除元素,而Queue
只有两个操作:
- 把元素添加到队列末尾;
- 从队列头部取出元素。
队列接口Queue
定义了以下几个方法:
int size()
:获取队列长度;
boolean add(E)
/boolean offer(E)
:添加元素到队尾;
E remove()
/E poll()
:获取队首元素并从队列中删除;
E element()
/E peek()
:获取队首元素但并不从队列中删除。
9、栈(Stack)是一种后进先出(LIFO:Last In First Out)的数据结构。
Stack
只有入栈和出栈的操作:
- 把元素压栈:
push(E)
; - 把栈顶的元素“弹出”:
pop(E)
; - 取栈顶元素但不弹出:
peek(E)
。
Stack的作用
Stack在计算机中使用非常广泛,
JVM在处理Java方法调用的时候就会通过栈这种数据结构维护方法调用的层次。
JVM会创建方法调用栈,每调用一个方法时,先将参数压栈,然后执行对应的方法;当方法返回时,返回值压栈,调用方法通过出栈操作获得方法返回值。
因为方法调用栈有容量限制,嵌套调用过多会造成栈溢出,即引发StackOverflowError
对整数进行进制的转换就可以利用栈