1. 集合
集合与数组的区别:
集合 | 数组 | |
---|---|---|
长度 | 可变 | 固定 |
存储的数据类型 | 对象的引用 | 基本数据类型 |
整个集合类的体系结构(简略):
1.1 Collection
Collection接口的常用方法:
返回值类型 | 方法 | 含义 |
---|---|---|
boolean | add(E e) | 将指定的对象添加到集合中 |
boolean | remove(Obiect o) | 将指定的对象从集合中删除 |
int | size() | 返回此集合中的元素数 |
boolean | isEmpty() | 判断集合是否为空 |
Iterator | iterator() | 返回此集合中的元素的迭代器,可用于遍历集合中的元素 |
boolean | contains(Object o) | 判断集合是否包含指定的元素 |
Object[] | toArray() | 将集合转换成数组 |
1.1.1 List
List接口存储一组不唯一(元素可以重复),有序(顺序与插入顺序一致)的对象
相对于Collection接口增加的方法:
返回值类型 | 方法 | 含义 |
---|---|---|
E | get(int index) | 返回集合中指定索引处的元素 |
void | add(int index,E e) | 将指定的元素添加到该集合中的指定位置上 |
int | indexOf(Object o) | 返回集合中指定元素第一次出现的索引,如果元素不存在,则返回-1 |
int | lastIndexOf(Object o) | 返回集合中指定元素最后一次出现的索引,元素不存在,则返回-1 |
E | remove(int index) | 移除列表中指定位置的元素, 返回的是被移除的元素 |
E | set(int index , E e) | 用指定元素替换集合中指定位置的元素,返回更新前的元素 |
1.1.1.1 ArrayList
ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问(根据索引位置)。当当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
1.1.1.2 LinkedList
LinkedList的底层是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。
1.1.1.3 Vector
Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。
三者之间的区别:
ArrayList | LinkedList | Vector | |
---|---|---|---|
底层实现 | 数组 | 循环双向链表 | 数组 |
是否线程安全 | 不安全 | 不安全 | 安全 |
优缺点 | 查询速度快,插入删除慢 | 插入删除快 | 速度慢 |
1.1.2 Set
Set接口存储一组唯一,无序的对象
它注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复。对象的相等性本质是对象hashCode值(java是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象视为相等的,就必须覆盖Object的hashCode方法和equals方法。
1.1.2.1 HashSet
HashSet的底层是HashMap(哈希表)支持的,他是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
//每一个HashSet的构造函数里面都实例了HashMap对象
public HashSet() {
map = new HashMap<>();
}
//省略一万字
.............
}
HashSet存储数据的原理:
在JDK1.8之前,哈希表的底层是采用数组+链表的方式实现的,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而在JDK1.8以后,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值 8 时,将链表转换为红黑树,这样大大减少了查找时间。
综上:HashSet首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals方法 如果 equls结果为true ,HashSet就视为同一个元素,舍弃其中一个。如果equals 为 false 就不是同一个元素,就会在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中),也就是哈希值一样的存一列,如果个数超过了8 ,链表结构 就会转化为红黑树结构。
思考:如果要存放自定义类型的对象 ,则该怎么存储 ?
解决办法:必须重写hashCode方法和equals方法
HashSet存放自定义类型的对象 :
public class Student implements Serializable {
private static final long serialVersionUID = 6030711162869997903L;
private Integer id;
private String username;
public Student(Integer id, String username) {
this.id = id;
this.username = username;
}
//重写hashCode方法和equals方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(id, student.id) &&
Objects.equals(username, student.username);
}
@Override
public int hashCode() {
return Objects.hash(id, username);
}
//省略get/set/toString方法
}
public static void main(String[] args) {
Set<Student> students = new HashSet<>();
students.add(new Student(1,"张三"));
students.add(new Student(1,"张三"));
students.add(new Student(2,"李四"));
Iterator<Student> iterator = students.iterator();
while (iterator.hasNext()){
Student student = iterator.next();
System.out.println(student);
}
}
没有重写hashCode和equals方法时,打印结果如下:Set集合里存放了重复的元素
重写hashCode和equals方法时,打印结果如下:
1.1.2.2 LinkedHashSet
HashSet里面存放的元素是无序的,但是他有一个子类:LinkedHashSet可以保证数据的有序存放。通过查看底层源码可知:它继承HashSet、又基于LinkedHashMap来实现的。LinkedHashSet底层使用LinkedHashMap来保存所有元素,它继承HashSet,其所有的方法操作上又与HashSet相同,因此LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个LinkedHashMap来实现,在相关操作上与父类HashSet的操作相同,直接调用父类HashSet的方法即可。
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
private static final long serialVersionUID = -2851667679971038690L;
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
//点进上面的super方法后,源码如下:
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
//由此可知,LinkedHashSet是基于HashSet和LinkedHashMap来实现的
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
public LinkedHashSet() {
super(16, .75f, true);
}
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
}
}
1.1.2.3 TreeSet
TreeSet是一个有序的集合,它的作用是提供有序的Set集合,但是读取数据速度慢。它继承了AbstractSet抽象类,实现了NavigableSet,Cloneable,Serializable接口,而NavigableSet实现了SortedSet接口。TreeSet的元素支持2种排序方式:自然排序或者根据提供的Comparator进行排序。
TreeSet相对于Set接口增加的方法:
返回值 | 方法 | 含义 |
---|---|---|
E | first() | 返回此集合中第一个(最低)元素 |
E | last() | 返回此集合中最后一个(最高)元素 |
Comparator | comparator() | 返回用于对该集合中的元素进行排序的比较器 |
SortedSet | headSet(E e) | 返回e之前(不包含e)的所有元素 |
SortedSet | subSet(E e2,E e2) | 返回e1到e2之间的所有元素,含e1不含e2 |
SortedSet | tailSet(E e) | 返回集合中大于或等于e的所以元素 |
注:凡是涉及到取中间元素的方法基本都是含左不含右
TreeSet是使用二叉树的原理对新add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。Integer和String对象都可以进行默认的TreeSet排序,而自定义类的对象是不可以的,自己定义的类必须实现Comparable接口,并且覆写相应的compareTo()函数,才可以正常使用。所以在使用TreeSet集合时,一定要保证存放的对象实现了Comparable接口并且实现了compareTo方法。
注:在重写conpareTo方法时,返回值不同集合遍历输出的元素顺序就不同。如果返回值为正整数,就认为新插入的元素比上一个元素大,于是二叉树存储时,会存在根的右侧;如果返回值为0,认为是相同的元素;如果返回值为负整数,就认为新插入的元素比上一个元素小,于是二叉树存储时,会存在根的左侧。特别注意:TreeSet遍历输出时默认采用的是中序遍历的方式
//Student.java
public class Student implements Comparable<Student>, Serializable {
private static final long serialVersionUID = 7527642451143166932L;
private String username;
private Integer age;
public Student(String username, Integer age) {
this.username = username;
this.age = age;
}
//省略get/set方法
...........
//重写compareTo方法 按照年龄排序
@Override
public int compareTo(Student o) {
//return this.age-o.age; 升序(递增) 可参照二叉树构造原理分析
return o.age - this.age; //逆序(递减)
}
@Override
public String toString() {
return "Student{" +
"username='" + username + '\'' +
", age=" + age +
'}';
}
}
//测试类
public static void main(String[] args) {
/**
* TreeSet集合中的元素必须实现Comparable接口,并实现compareTo方法
* TreeSet最终是采用中序的遍历方式遍历输出集合中元素的
*/
Set<Student1> students = new java.util.TreeSet<>();
students.add(new Student1("张三",15));
students.add(new Student1("李四",25));
students.add(new Student1("王五",18));
Iterator<Student1> iterator = students.iterator();
while (iterator.hasNext()){
Student1 stu = iterator.next();
System.out.println(stu);
}
}
打印结果如下:
1.2 Map
Map接口提供的是key到value的映射。Map集合中不能包含相同的key,并且每个key只能映射一个value,且允许值为null。
Map接口中常用的方法:
返回值 | 方法 | 含义 |
---|---|---|
V | put(K key,V value) | 把指定的键与指定的值添加到Map集合中 |
V | get(Object key) | 根据指定的键,在Map集合中获取对应的值 |
V | remove(Object key) | 移除Map集合中指定的元素 |
boolean | containsKey(Object key) | 判断Map集合中是否包含指定的键 |
boolean | containsValue(Object value) | 判断Map集合中是否包含指定的值 |
Set | keySet() | 获取Map集合中所有的键 |
Collection | values() | 获取Map集合中所有的值 |
Set<Map.Entry<K,value>> | entrySet() | 获取到Map集合中所有的键值对对象的集合 |
1.2.1 HashMap
底层实现:HashMap的底层实现同HashSet一样,1.8之后采用 数组+链表+红黑树
HashMap根据键的hashCode值存储数据,当给HashMap中存放自定义对象时,如果自定义对象作为key存在,这时要保证对象唯一,必须覆写对象的hashCode和equals方法。大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null(HashMap允许键和值为null)。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
ConcurrentHashMap:ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁。简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全,而且访问速度也不慢。
JDK1.7 ConcurrentHashMap的结构:
concurrencyLevel:并行级别、并发数、Segment 数。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。
JDK1.8 ConcurrentHashMap的结构:在7的基础上加上了红黑树,并且舍弃了Segment+ReentrantLock,采用CAS+Synchronized来实现并发安全性。
CAS(Compare And Swap):比较并且替换,是乐观锁的一种实现方式,是一种轻量级锁。
1.2.2 LinkedHashMap
LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,使得map中存放的key和取出的顺序一致。底层实现采用的就是链表+哈希表。
1.2.3 TreeMap
TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap时传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
1.2.4 HashTable
Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,且键和值不允许为空,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
1.3 Iterator
在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口Iterator,Iterator接口也是Java集合中的一员,但他与Collection、Map接口有所不同,Collection接口与Map接口主要用于存储元素,而Iterator主要用于迭代访问(即遍历)Collection中的元素,因此Iterator对象也被称为迭代器。
主要方法:
返回值 | 方法 | 含义 |
---|---|---|
boolean | hashNext() | 判断集合中是否有元素,如果仍有元素可以迭代,则返回 true |
E | next() | 返回迭代的下一个元素 |
1.4 Collections工具类
java.utils.Collections是集合工具类,用来对集合进行操作。
常用的方法:
返回值 | 方法 | 含义 |
---|---|---|
boolean | addAll(Collection c, T… elements) | 往集合中添加一些元素 |
void | shuffle(List list) | 打乱集合中元素的顺序 |
void | sort(List list) | 将集合中元素按照默认规则排序(升序) |
void | sort(List list,Comparator<? super T> ) | 将集合中元素按照指定规则排序 |
Map<K,V> | synchronizedMap(Map<K,V> m) | 返回一个线程安全的Map集合 |
public class CollectionsDemo {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
//原来写法
//list.add(5);
//list.add(222);
//list.add(1);
//list.add(2);
//采用工具类 完成 往集合中添加元素
Collections.addAll(list, 5, 222, 1,2);
System.out.println(list);
//排序方法
Collections.sort(list);
System.out.println(list);
}
}
结果:
[5, 222, 1, 2]
[1, 2, 5, 222]
由上可知:sort方法默认的排序规则是升序,如果想实现降序,就需要用到Comparator比较器。
Comparator比较器:需要实现compare方法
public static void main(String[] args) {
List<Person> persons = new ArrayList<>();
persons.add(new Person("张三",21));
persons.add(new Person("李四",17));
persons.add(new Person("王五",19));
//降序
/**
* o1 - o2:升序
* o2 - o1:降序
*/
Collections.sort(persons, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o2.getAge()-o1.getAge();
}
});
Iterator<Person> iterator = persons.iterator();
while(iterator.hasNext()){
Person person = iterator.next();
System.out.println(person);
}
}
Comparable和Comparator两个接口的区别:
Comparable:强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
Comparator:强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。