集合
一、前言
集合框架体系
Collection接口特点方法
Collection接口的子接口:
List实现类:
ArrayList、LinkedList、Vector
Set实现类:
HashSet、LinkedHashSet
Map接口、特点方法、遍历方式
Map接口的实现类:
HashMap、Hashtable
等Collections工具类的使用
集合与数组
- 数组:
- 长度开始时必须指定,而且一旦指定,不能更改
- 保存的必须为同一类型的元素
- 使用数组进行增加元素的示意代码-比较麻烦
- 集合
- 可以动态保存任意多个对象,使用比较方便
- 提供了一系列方便的操作对象的方法:
add、remove、set、get
等 - 使用集合添加,删除新元素的代码简洁
- 数组:
集合体系图
Collection
体系图(单列集合:Collection
重要子接口List和Set
,实现子类都是单列集合)Map
体系图(双列集合:Map
接口的实现子类是双列集合,存放的 K-V)
二、Collection接口
Collection接口实现类的特点
Collection
实现子类可以存放多个元素,每个元素可以是Object
;- 有些
Collection
的实现类,可以存放重复的元素,有些不可以; - 有些
Collection
的实现类,有些是有序的(List
),有些不是有序(Set
); Collection
接口没有直接的实现子类,是通过它的子接口Set和List来
实现的
Collection接口的常用方法
以实现子类ArrayList
为例(Collection接口不能直接实例化)
add
:添加单个元素,list.add("Jack");
remove
:删除指定元素,list.remove(0);
或者list.remove("Jack");
contains
:查找元素是否存在,list.contains("Jack");
size
:获取元素个数isEmpty
:判断是否为空clear
:清空addAll
:添加多个元素,创建新的list2
,然后list.addAll(list2);
containsAll
:查找多个元素是否都存在,创建新的list2
,然后list.containsAll(list2);
removeAll
:删除多个元素,创建新的list2
,然后list.removeAll(list2);
Collection接口遍历元素的方式
使用Iterator
迭代器
基本介绍
Iterator
对象称为迭代器,主要用于遍历Collection
集合中的元素;- 所有实现了
Collection
接口的集合类都有一个iterator()
方法,用以返回一个实现了Iterator
接口的对象,即可以返回一个迭代器; Iterator
的结构:有while(iterator.hasNext())
判断,为true
时,指针下移;Iterator
仅用于遍历集合,Iterator
本身并不存放对象。
代码演示
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class test1 {
public static void main(String[] args) {
Collection collection = new ArrayList();
collection.add(new Book("西游记","吴承恩"));
collection.add(new Book("红楼梦","曹雪芹"));
collection.add(new Book("三国演义","罗贯中"));
collection.add(new Book("水浒传","施耐庵"));
System.out.println(collection);
//输出:[Book{name='西游记', author='吴承恩'}, Book{name='红楼梦', author='曹雪芹'}, Book{name='三国演义', author='罗贯中'}, Book{name='水浒传', author='施耐庵'}]
//1. 用迭代器 Iterator 遍历collection集合
//先得到collection的迭代器
Iterator iterator = collection.iterator();
//使用while遍历,快捷键 itit (ctrl+J:所有快捷键)
while (iterator.hasNext()) {
Object book = iterator.next();
System.out.println("书本:"+book);
//输出:
//书本:Book{name='西游记', author='吴承恩'}
//书本:Book{name='红楼梦', author='曹雪芹'}
//书本:Book{name='三国演义', author='罗贯中'}
//书本:Book{name='水浒传', author='施耐庵'}
}
//2. 当while结束后,此时迭代器指向最后一个元素,若再次下移,则抛出异常
//iterator.next();//NoSuchElementException
//3. 如果需要再次遍历,则需要重置迭代器
iterator = collection.iterator();
//第二遍遍历
System.out.println("第二遍遍历");
while (iterator.hasNext()) {
Object book = iterator.next();
System.out.println("书本:"+book);
}
}
}
class Book
{
String name;
String author;
Book(String name,String author){
this.name = name;
this.author = author;
};
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", author='" + author + '\'' +
'}';
}
}
增强for循环
import java.util.ArrayList;
import java.util.Collection;
public class test1 {
public static void main(String[] args) {
Collection collection = new ArrayList();
collection.add(new Book("西游记","吴承恩"));
collection.add(new Book("红楼梦","曹雪芹"));
collection.add(new Book("三国演义","罗贯中"));
collection.add(new Book("水浒传","施耐庵"));
System.out.println(collection);
//输出:[Book{name='西游记', author='吴承恩'}, Book{name='红楼梦', author='曹雪芹'}, Book{name='三国演义', author='罗贯中'}, Book{name='水浒传', author='施耐庵'}]
//1.增强for循环遍历列表(底层仍然是迭代器,可以理解为简化版迭代器)
//快捷键 I
for(Object book : collection)
{
System.out.println("书本:"+book);
//输出
//书本:Book{name='西游记', author='吴承恩'}
//书本:Book{name='红楼梦', author='曹雪芹'}
//书本:Book{name='三国演义', author='罗贯中'}
//书本:Book{name='水浒传', author='施耐庵'}
}
//2. 增强for循环也可使用在数组遍历中
int nums[] = {3,6,1,5,9,35};
for(int i : nums)
{
System.out.println(i);
}
}
}
class Book
{
String name;
String author;
Book(String name,String author){
this.name = name;
this.author = author;
};
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", author='" + author + '\'' +
'}';
}
}
三、List
List接口基本介绍
- List接口是Collection接口的子接口;
- List集合类中元素有序(添加顺序和取出顺序一致),且可一致;
- List集合中的每个元素都有其对应的顺序索引,即支持索引;
- List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可根据序号存取容器中的元素;
- JDK API中List接口实现的类有:
ArrayList、LinkedList和Vector
等。
List常用方法
void add(int index,Object ele)
:在index位置插入ele元素;boolean addAll(int index, Collection eles)
:在index位置开始将eles中的所有元素添加进来;Object get(int index)
:获取指定index位置的元素;int indexOf(Object obj)
:返回obj在集合中首次出现的位置;int lastIndexOf(Object obj)
:返回obj在当前集合中末次出现的位置;Object remove(int index)
:移除指定index位置的元素,并返回此元素;Object set(int index, Object ele)
:设置指定index位置的元素为ele,相当于是替换(要替换的元素必须存在);List subList(int fromlndex, int tolndex)
:返回fromlndex到tolndex位置的子集合,左闭右开。
三种遍历方式
- 方式一:使用
iterator
(itit); - 方式二:普通for循环;
- 方式三:增强for循环。
- 方式一:使用
对集合的冒泡排序
//根据书的价格从小到大排序
import java.util.ArrayList;
import java.util.List;
public class test1 {
public static void main(String[] args) {
List list = new ArrayList();
list.add(new Book("西游记",89.1,"吴承恩"));
list.add(new Book("红楼梦",59.3,"曹雪芹"));
list.add(new Book("水浒传",90.3,"施耐庵"));
list.add(new Book("三国",46.0,"罗贯中"));
for(Object book : list)
{
System.out.println(book);
}
//对集合进行冒泡排序
sort(list);
System.out.println("======排序后======");
for(Object book : list)
{
System.out.println(book);
}
}
public static void sort(List list){
int n = list.size();
for (int i = 0; i < n - 1; i++) {
for(int j =0;j < n - 1 - i;j++)
{
//取出对象
Book b1 = (Book) list.get(j);
Book b2 = (Book) list.get(j+1);
if(b1.price > b2.price)
{
list.set(j,b2);
list.set(j+1,b1);
}
}
}
}
}
class Book
{
String name;
String author;
double price;
Book(String name,double price,String author){
this.name = name;
this.author = author;
this.price = price;
};
@Override
public String toString() {
return "书名:"+ name + "\t\t价格:"+price+"\t\t作者:"+author;
}
}
//输出:
//书名:西游记 价格:89.1 作者:吴承恩
//书名:红楼梦 价格:59.3 作者:曹雪芹
//书名:水浒传 价格:90.3 作者:施耐庵
//书名:三国 价格:46.0 作者:罗贯中
//======排序后======
//书名:三国 价格:46.0 作者:罗贯中
//书名:红楼梦 价格:59.3 作者:曹雪芹
//书名:西游记 价格:89.1 作者:吴承恩
//书名:水浒传 价格:90.3 作者:施耐庵
ArrayList
- ArrayList的注意事项
ArrayList
可以加入多个、任何元素,包括null;ArrayList
是由数组来实现数据存储的;ArrayList
基本等同于Vector,除了ArrayList是线程不安全(执行效率高),由源码值并无synchronized
,在多线程情况下,不建议使用ArrayList。
- ArrayList扩容机制(使用IDEA的Debug功能看源码)
ArrayList中维护了一个Object类型的数组elementData;
transient Object[] elementData;
//transient 表示短暂的,表示该属性不会被序列化当创建ArrayList对象时,如果使用的是无参构造器
ArrayList()
,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍;ArrayLis()无参构造器扩容源码分析图ArrayLis()无参构造器扩容源码分析图
如果使用的是指定大小的构造器
ArrayList(int n)
,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍。
Vector
Veactor的注意事项
- Vector底层也是一个对象数组:
protect Object[] elementDate;
- Vector是线程安全的,Vector类的操作方法带有
synchronized
; - 在开发中,需要线程安全是,考虑用Vector。
- Vector底层也是一个对象数组:
Vector和ArrayList的比较
底层结构 版本 线程安全及效率 扩容参数 ArrayList 可变数组 JDK1.2 不安全,效率高 有参构造:1.5
无参构造:先10,满后1.5倍Vector 可变数组 JDK1.0 安全,效率低 有参构造:2
无参构造:先10,满后2倍
LinkList
- 基本情况
- LinkList底层实现了双向链表和双端队列特点;
- 可以添加任意元素(可重复),包括null;
- 线程不安全,没有实现同步。
- 底层结构
- LinkedList底层维护了一个双向链表;
- LinkedList中维护了两个属性first和last分别指向首节点和尾节点;
- 每个节点(Node对象)里面又维护了
prev,next,item
三个属性,其中通过prev指向前一个,通过next指向后一个节点。最终实现双向链表; - 所以LinkedList的元素的添加和删除,不是通过数组完成的,相对来说效率较高。
ArrayList和LinkList
ArrayList和LinkList的比较
底层结构 增删效率 改查效率 ArrayList 可变数组 较低(数组扩容) 较高 LinkList 双向链表 较高(链表加节点) 较低 如何选取ArrayList和LinkList
- 如果我们改查的操作多,选择ArrayList;
- 如果我们增删的操作多,选择LinkedList;
- 一般来说,在程序中,80%-90%都是查询,因此大部分情况下会选择ArrayList;
- 在一个项目中,根据业务灵活选择,也可能这样,一个模块使用的是ArrayList,另外一个模块是LinkedList,也就是说,要根据业务来进行选择。
四、Set
- 基本介绍
- 无序(添加和取出的顺序比一至),没有索引;
- 不允许出现重复元素,所以最多包含一个null;
JDK API
中Set接口实现的类由HashSet、TreeSet
等。
- 常用方法
- 和List接口一样,Set接口也是Collection的子接口,因此常用方法和Collection接口一样。
- 遍历方式
- 迭代器;
- 增强for循环;
- 不能使用索引。
HashSet
基本介绍
- HashSet实现了Set接口;
- HashSet实际上是
HashMap
; - 可以存放null值,但是只能有一个null;
- HashSet不保证元素是有序的,取决于hash后,再确定索引的结果(即,不保证存放元素的顺序和取出顺序一致);
- 不能有重复元素或对象。
底层原理
- HashSet 底层 HashMap;
- 添加一个元素时,先得到hash值(算法:
(h = key.hashCode()) ^ (h >>> 16)
,不是简单的hashCode()得出的。
),再会转成索引值i(i = (n - 1) & hash
); HashSet
中,元素储存的位置与元素的hash值和当前集合容器的个数有关(tab[(n-1)&hash]
);- 找到存储数据表table,看这个索引位置是否已经存放的有元素:
- 如果没有,直接加入;
- 如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后;
扩容机制
- HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(
threshold
)是16,加载因子(loadFactor
)是0.75,故第二次扩容临界值为12; - 如果table数组使用到了临界值12,就会扩容到
16*2=32
,新的临界值就是32 * 0.75 = 24
,依次类推; - 在Java8中,如果一条链表的元素个数到达
TREEIFY THRESHOLD
(默认是8),并且table的大小>=MIN TREEIFY CAPACITY
(默认64),就会进行树化(红黑树),否则依然采用数组扩容机制。
- HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(
LinkedHashSet
- 基本说明
- LinkedHashSet 是HashSet 子类;
- LinkedHashSet 底层是一个
LinkedHashMap
,底层维护了一个数组+双向链表; - LinkedHashSet 根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的;
- LinkedHashSet 不允许添重复元素。
- 底层原理
- 在LinkedHastSet中维护了一个hash表和双向链表(LinkedHashSet有
head
和tail
属性); - 每个节点由
befor
和after
,属性,这样就构成了双向链表; - 在添加一个元素时,先求hash值,再求索引,确定该元素在table的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加[原则和hashset一样]);
- 这样的话,我们遍历LinkedHashSet也能确保插入顺序和遍历顺序一致(有序性)。
- 在LinkedHastSet中维护了一个hash表和双向链表(LinkedHashSet有
五、Map
Map接口实现类的特点(使用实现类 HashMap)
- Map 与 Collection 并列存在。用于保存具有映射关系的数据:Key-Value(双列元素);
- Map 中的 key 和 value 可以是任何引用类型的数据,会封装到
HashMap$Node
对象中; - Map 中的 key 不允许重复,原因和 HashSet 一样,若key重复,value不重复,则相当于替换,即用新的value替换原来的value;
- Map 中的 value 可以重复;
- Map 的 key 可以为 null, value 也可以为 null,注意:key 只能有一个 null,value 可以有多个为 null;
- 常用 String 类作为 Map 的 key,但key可以用Object类;
- key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value。
Map中
key-value
存放位置解读一对k-v是放在一个HashMap$Node中的,
HashMap$Node node = newNode(hash,key, value,null)
;为了方便程序员的遍历,还会创建EntrySet集合,该集合存放的元素的类型Entry,而一个Entry对象有K和V,存放的位置如下:
transient Set<Map.Entry<K,V>> entrySet
,但enytrySet
中的K-V只是引用了HashMap$Node
中的K-V;entrySet中,定义的类型是Map.Entry,但是实际上存放的还是
HashMap$Node
这是因为实现接口:static class Node<K,V> impLements Map.Entry<K,V>
;当把HashMap$Node对象存放到entrySet就方使我们的遍历,因为Map.Entry提供了重要方法
K:getKey(); V:getValue();
。图示:
代码调试:
Map常用方法
remove
:根据键删除映射关系,map.remove(null);
get
:根据键获取值,Object val = map.get("1");
size
:获取元素个数,System.out.println("k-v=" + map.size());
isEmpty
:判断个数是否为0,System.out.println(map.isEmpty());
clear
:清除 k-v,map.clear();
containsKey
:查找键是否存在,System.out.println("结果=" + map.containsKey("1"));
Map的六种遍历方式
- 第一组:先取出所有的Key(
map.keySet()
), 通过 Key 取出对应的 Value- 增强
for
循环:遍历keySet
中的所有key,根据key取出value(map.get(key)
); - 迭代器:用
Iterator
遍历keySet
中的所有key,根据key取出value(map.get(key)
)。
- 增强
- 第二组:直接把所有的values(
map.values()
)取出,再遍历- 增强
for
循环:遍历value
中的所有value; - 迭代器:用
Iterator
遍历value
中的所有value。
- 增强
- 第三组:通过EntrySet(
map.entrySet()
)来获取 k-v(向下转型:Map.Entry m = (Map.Entry) entry;
)- 增强
for
循环:遍历entrySet
中的所有对象,根据m.getKey()
取出key,根据m.getValue()
取出value; - 迭代器:用
Iterator
遍历entrySet
中的所有对象,根据m.getKey()
取出key,根据m.getValue()
取出value。
- 增强
- 代码演示:
- 第一组:先取出所有的Key(
package demo;
import java.util.*;
public class test1 {
@SuppressWarnings("all")
public static void main(String[] args) {
Map map = new HashMap<>();
map.put("1","hello");
map.put("2","world");
map.put("3","你好");
map.put("4","世界");
map.put("5","!");
System.out.println(map);
//输出:{1=hello, 2=world, 3=你好, 4=世界, 5=!}
//第一组:先取出所有的Key(`map.keySet()`), 通过 Key 取出对应的 Value
System.out.println("=====第一组=====");
Set keySet = map.keySet();
//1. 增强for循环
System.out.println("增强for循环:");
for(Object o : keySet)
{
System.out.println(map.get(o));
}
//2. 迭代器
System.out.println("迭代器:");
Iterator iterator1 = keySet.iterator();
while (iterator1.hasNext()) {
Object next = iterator1.next();
System.out.println(map.get(next));
}
//第二组:直接把所有的values(`map.values()`)取出,再遍历
System.out.println("=====第二组=====");
Collection values = map.values();
//1. 增强for循环
System.out.println("增强for循环:");
for(Object o : values)
{
System.out.println(o);
}
//2. 迭代器
System.out.println("迭代器:");
Iterator iterator2 = values.iterator();
while (iterator2.hasNext()) {
Object next = iterator2.next();
System.out.println(next);
}
//第三组:通过EntrySet(`map.entrySet()`)来获取 k-v(向下转型:`Map.Entry m = (Map.Entry) entry;`)
System.out.println("=====第三组=====");
Set entrySet = map.entrySet();//EntrySet<Map.Entry<K,V>>
//1. 增强for循环
System.out.println("增强for循环:");
for(Object entry : entrySet)
{
//将 entry 转成 Map.Entry
Map.Entry m = (Map.Entry) entry;
System.out.println("key: "+m.getKey()+";value: "+m.getValue());
}
//2. 迭代器
System.out.println("迭代器:");
Iterator iterator3 = entrySet.iterator();
while (iterator3.hasNext()) {
Object next = iterator3.next();
//向下转型成 Map.Entry
Map.Entry m = (Map.Entry) next;
System.out.println("key: "+m.getKey()+";value: "+m.getValue());
}
}
}
/*
输出:
{1=hello, 2=world, 3=你好, 4=世界, 5=!}
=====第一组=====
增强for循环:
hello
world
你好
世界
!
迭代器:
hello
world
你好
世界
!
=====第二组=====
增强for循环:
hello
world
你好
世界
!
迭代器:
hello
world
你好
世界
!
=====第三组=====
增强for循环:
key: 1;value: hello
key: 2;value: world
key: 3;value: 你好
key: 4;value: 世界
key: 5;value: !
迭代器:
key: 1;value: hello
key: 2;value: world
key: 3;value: 你好
key: 4;value: 世界
key: 5;value: !
*/
HashMap
基本说明
- Map接口的常用实现类:
HashMap
,Hashtable
和Properties
; - HashMap是Map接口使用频率最高的实现类;
- HashMap 以key-val 对的方式来存储数据(HashMap$Node类型);
- key不能重复,但是值可以重复,允许使用null键和null值;
- 如果添加相同的key,则会覆盖原来的
key-val
,等同于修改,(key不会替换, val会替换)与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的(JDK8的hashMap底层数组+链表+红黑树); - HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有
synchronized
。
- Map接口的常用实现类:
HashMap底层机制
- HashMap底层维护了Node类型的数组table,默认为
null
- 当创建对象时,将加载因子(
loadfactor
)初始化为0.75; - 当添加key-val时,通过key的哈希值得到在table的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key和准备加入的key是否相等,如果相等,则直接替换val;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容;
- 第1次添加,则需要扩容table容量为16,临界值(threshold)为12(16*0.75)以后再扩容,则需要扩容table容量为原来的2倍(32),临界值为原来的2倍,即24,依次类推;
- 在Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且
table.size()>= MIN TREEIFY CAPACITY(默认64)
,就会进行树化(红黑树)。
- HashMap底层维护了Node类型的数组table,默认为
HashMap扩容树化
如果table长度小于常量
MIN_TREEIFY_CAPACITY
时,不会变为红黑树,而是调用resize()
方法进行扩容。MIN_TREEIFY_CAPACITY
的默认值是64。显然HashMap认为,虽然链表长度超过了8,但是table长度太短,只需要扩容然后重新散列一下就可以。当同时满足链表长度超过8,table长度超过64才会树化。
HashTable
基本介绍
- 存放的元素是键值对:即K-V;
- hashtable的键和值都不能为null,否则会抛出
NullPointerException
; - hashTable 使用方法基本上和HashMap一样;
- hashTable 是线程安全的(
synchronized
),hashMap是线程不安全的。
底层机制
- 底层有数组 Hashtable$Entry[],初始化大小为11;
- 临界值:
table.size()*0.75
; - 扩容:
int newCapacity = (oldCapacity << 1) + 1;
即原来容量大小的2倍加1。
HashTable和HashMap的比较
版本 线程安全 效率 是否允许null HashMap 1.2 不安全 高 允许 HashTable 1.0 安全 较低 不允许
Properties
- 基本介绍
Properties
类继承自Hashtable
类并且实现了Map接口,也是使用一种键值对的形式来保存数据;- 使用特点和
Hashtable
类似; - Properties继承了
Hashtable
,可任意通过k-v存放数据,key和value都不能为null
。 - Properties 还可以用于从
xxx.properties
文件中,加载数据到Properties类对象,并进行读取和修改; - 说明:
xxx.properties
文件通常作为配置文件,IO流中详细说明;
六、TreeSet和TreeMap
- TreeSet(单列)
- 当我们使用无参构造器,创建 TreeSet 时,仍然是无序的;
- 使用 TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类)并指定排序规则;
- 自定义排序代码如下:
package demo;
import java.util.*;
public class test1 {
@SuppressWarnings("all")
public static void main(String[] args) {
//用实现 Comparator 接口的匿名内部类实现比较器
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//按照字符串首字母的大小进行排序
//return ((String)o1).compareTo((String) o2);//升序
//[aq, befee, c, def]
return ((String)o2).compareTo((String) o1);//降序
//[def, c, befee, aq]
//按照字符串的长度进行排序
//return ((String) o1).length() - ((String) o2).length();//升序
//[c, aq, def, befee]
//return ((String) o2).length() - ((String) o1).length();//降序
//[befee, def, aq, c]
}
});
treeSet.add("aq");
treeSet.add("def");
treeSet.add("befee");
treeSet.add("c");
System.out.println(treeSet);
}
}
- TreeMap(双列)
- 使用默认的构造器,创建 TreeMap, 是无序的(也没有排序);
- 使用 TreeMap 提供的一个构造器,可以传入一个比较器(匿名内部类)并指定排序规则,此处为键(Key)排序;
- 排序代码:
package demo;
import java.util.*;
public class test1 {
@SuppressWarnings("all")
public static void main(String[] args) {
TreeMap treeMap = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//按照字符串首字母的大小进行排序
//return ((String)o1).compareTo((String) o2);//升序
//{aq=1, befee=3, c=4, def=2}
//return ((String)o2).compareTo((String) o1);//降序
//{def=2, c=4, befee=3, aq=1}
//按照字符串的长度进行排序
//return ((String) o1).length() - ((String) o2).length();//升序
//{c=4, aq=1, def=2, befee=3}
return ((String) o2).length() - ((String) o1).length();//降序
//{befee=3, def=2, aq=1, c=4}
}
});
treeMap.put("aq",1);
treeMap.put("def",2);
treeMap.put("befee",3);
treeMap.put("c",4);
System.out.println(treeMap);
}
}
七、集合的选择
判断储存的类型(一组对象(单列)或一组键值对(双列))
- 一组对象(单列):Collection接口
- 允许重复:List
- 增删多:LinkList(底层维护了一个双向链表)
- 改查多:ArrayList(底层维护了一个Object类的可变数组)
- 不允许重复:Set
- 无序:HashSet(底层是HashMap,维护了一个哈希表,即数组+链表+红黑树)
- 排序:TreeSet
- 插入和取出顺序一致:LinkedHashSet,维护数组+双向链表
- 允许重复:List
- 一组键值对(双列):Map
- 键无序:HashMap(底层是哈希表,JDK7:数组+链表;JDK8:数组+链表+红黑树)
- 键排序:TreeMap
- 键插入和取出顺序一致:LinkedHashMap
- 读取文件:Properties
八、Collection工具类
- Collections 是一个操作 Set, List 和Map等集合的工具类;
- Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作。
排序(均为静态方法)
reverse(List)
:反转List 中元素的顺序;shuffle(List)
:对List集合元素进行随机排序;sort(List)
:根据元素的自然顺序对指定List集合元素按升序排序;sort(List,Comparator)
:根据指定的Comparator产生的顺序对List集合元素进行排序;swap(List, int, int)
:将指定list集合中的i处元素和j处元素进行交换。
查找和替换
Object max(Collection)
:根据元素的自然顺序,返回给定集合中的最大元素;Object max(Collection, Comparator)
:根据 Comparator指定的顺序,返回给定集合中的最大元素;Object min(Collection)
:根据元素的自然顺序,返回给定集合中的最小元素;Object min(Collection, Comparator)
:根据 Comparator指定的顺序,返回给定集合中的最小元素;int frequency(Collection, Object)
:返回指定集合中指定元素的出现次数;void copy(List dest,List src)
:将src中的内容复制到dest中;boolean replaceAll(List list,Object oldVal,Object newVal)
:使用新值替换List对象的所有旧值。
泛型
一、泛型的介绍:
泛型的引入
有如下需求:在一个ArrayList中创建添加n个指定类的对象(如Dog),并遍历输出。
传统方法:
ArrayList arrayList = newArrayList();
遍历过程:
Dog-加入-> Object-取出-> Dog
//放入到ArrayList 会先转成 Object,在取出时,还需要转换成Dog
缺点:不能对加入到集合ArrayList中的数据类型进行约束(不安全),遍历的时候,需要进行类型转换,如果集合中的数据量较大,对效率有影响。
使用泛型:
ArrayList<Dog> arrayList = newArrayList<Dog>();
遍历过程:
Dog-> Dog-> Dog
//放入时,和取出时,不需要类型转换,提高效率
优点:编译时,检查添加元素的类型,提高了安全性减少了类型转换的次数,提高效率。
泛型的基本介绍
- 泛(广泛)型(类型) =>
Integer,String,Dog
; - 泛型又称参数化类型,是Jdk5.0出现的新特性,解决数据类型的安全性问题;
- 在类声明或实例化时只要指定好需要的具体的类型即可;
- Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。同时,代码更加简洁、健壮;
- 泛型的作用是:可以在类声明时通过一个标识表示类中某个属性的类型,或者是某个方法的返回值的类型,或者是参数类型。
代码举例:
- 泛(广泛)型(类型) =>
package demo;
public class test1 {
@SuppressWarnings("all")
public static void main(String[] args) {
Person<Integer> person1 = new Person<Integer>(1);
Person<String> person2 = new Person<String>("1");
System.out.println(person1.getNums().equals(person2.getNums()));
//输出:false
//person1.getNums():Integer类型
//person2.getNums():String类型
}
}
class Person<E>
{
E nums;
public Person(E nums) {
this.nums = nums;
}
public E getNums() {
return nums;
}
}
二、泛型的语法
泛型的声明
interface接口<T>{}
和class类<K,V>{}
- 其中,T,K,V不代表值,而是表示类型;
- 任意字母都可以。常用T表示,是Type的缩写。
泛型的实例化
- 要在类名后面指定类型参数的值(类型)。如:
List<String> strList = new ArrayList<String>()
List<String> strList = new ArrayList<>()
Iterator<Customer> iterator = customers.iterator()
Map Hashmap<String,Dog> map = new Map<>();
- 要在类名后面指定类型参数的值(类型)。如:
泛型使用的注意事项和细节
- 在声明时,
< >
中的T、K、E等只能是引用类型,不能是基本数据类型int、double
等; - 在给泛型指定具体类型后,可以传入该类型或者其子类类型;
- 如果我们这样写
List list3 = new ArrayList();
默认给它的泛型是<E>
,E就是Object
。
- 在声明时,
三、自定义泛型
- 自定义泛型类
- 基本语法:
class 类名 <T/R...>{}
//表示可以有多个泛型 - 使用细节
- 普通成员可以使用泛型(属性、方法);
- 使用泛型的数组,不能初始化,因为数组在
new
时不能确定T的类型,就无法在内存开空间,只能 是先声明:T []ts
; - 静态方法中不能使用类的泛型,因为静态是和类相关的,二泛型是在对象创建的时候才明确,在类加载时,对象还没有创建,所以,如果静态方法和静态属性使用了泛型,JVM就无法完成初始化;
- 泛型类的类型,是在创建对象时确定的(因为创建对象时,需要指定确定类型);
- 如果在创建对象时,没有指定类型,默认Object;
- 基本语法:
- 自定义泛型接口
- 基本语法:
interface 接口名 <T/R...>{}
//表示可以有多个泛型 - 使用细节
- 接口中,静态成员也不能使用泛型(接口中所有成员都是静态的);
- 泛型接口的类型,在继承接口或者实现接口时确定;
- 没有指定类型,默认为
Object
。
- 基本语法:
- 自定义泛型方法
- 基本语法:
修饰符 <T,R..> 返回类型 方法名(参数列表){}
; - 使用细节
- 泛型方法,可以定义在普通类中,也可以定义在泛型类中;
- 当泛型方法被调用时,类型会确定;
public void eat(E e){}
,修饰符后没有<T,R..>,eat方法不是泛型方法,而是使用了泛型。
- 基本语法:
四、泛型的继承和通配符
- 说明
- 泛型不具备继承性,
错误;List<Object> list = newArrayList<String>();
<?>
:支持任意泛型类型;<? extend A>
:支持A类及A类的子类,规定了泛型的上限;<? super A>
:支持A类及A类的父类,不限于直接父类,只规定了泛型的下限。
- 泛型不具备继承性,
五、JUnit
JUnit是一个Java语言的单元测试框架,多数Java的开发环境都已经集成了JUnit作为单元测试的工具。使用方法:
new 类名.f1();
代码演示:
package demo;
public class test1 {
@SuppressWarnings("all")
public static void main(String[] args) {
new test1().f1();
new test1().f2();
new test1().f3();
}
void f1() {
System.out.println("方法一被调用!");
}
void f2() {
System.out.println("方法二被调用!");
}
void f3() {
System.out.println("方法三被调用!");
}
}
/*
输出:
方法一被调用!
方法二被调用!
方法三被调用!
*/
反射
引入:
有如下需求:根据配置文件re.properties指定信息,创建Cat对象并调用方法
hi
;配置文件
re.properties
内容为下:classfullpath=reflection.Cat method=hi
这样的需求在框架中特别多,即通过外部文件配置,在不修改源码情况下来控制程序,也符合设计模式的ocp原则(开闭原则:不修改源码,扩容功能)
传统方法需要new处对象后通过对象调用方法,但在配置文件中不能使用,需要用到反射,下面用代码演示。
反射机制
基本介绍
- 反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息(比如成员变量,构造器,成员方法等等),并能操作对象的属性及方法。反射在设计模式和框架底层都会用到;
- 加载完类之后,在堆中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了类的完整结构信息,通过这个对象得到类的结构。这个Class对象就像一面镜子,透过这个镜子看到类的结构,所以,形象的称之为:反射。
Java反射机制原理示意图
反射的功能
- 在运行时判断任意一个对象所属的类;
- 在运行时构造任意一个类的对象,使用构造器;
- 在运行时得到任意一个类所具有的成员变量和方法;
- 在运行时调用任意一个对象的成员变量和方法;
- 生成动态代理。
反射相关的主要类
java.lang.Class
:代表一个类,Class对象表示某个类加载后在堆中的对象;java.lang.reflect.Method
:代表类的方法,Method对象表示某个类的方法;java.lang.reflect.Field
:代表类的成员变量,Field对象表示某个类的成员变量(getField
不能得到私有的属性);java.lang.reflect.Constructor
:代表类的构造方法,Constructor对象表示构造器。
代码演示
package reflection;
import java.io.FileReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Properties;
public class test {
public static void main(String[] args) throws Exception{
//读取配置文件,访问类的信息
Properties properties = new Properties();
properties.load(new FileReader("src//reflection//re.properties"));
String classfullpath = properties.get("classfullpath").toString();
String methodName = properties.getProperty("method").toString();
System.out.println(classfullpath);//reflection.Cat
System.out.println(methodName);//hi
//使用反射
//(1) 加载类, 返回 Class 类型的对象 cls
Class cls = Class.forName(classfullpath);
//(2) 通过 cls 得到你加载的类 com.reflection.Cat 的对象实例
//此时o就相当于Cat类的一个实例化对象
Object o = cls.getDeclaredConstructor().newInstance();
//getDeclaredConstructor()方法会根据他的参数对该类的构造函数进行搜索并返回对应的构造函数,没有参数就返回该类的无参构造函数,然后再通过newInstance进行实例化。
System.out.println("o的运行类型:"+ o.getClass());
//输出:o的运行类型:class reflection.Cat
//(3) 通过 cls 得到你加载的类 com.reflection.Cat 的 methodName"hi" 的方法对象,即方法可以看作对象
Method method1 = cls.getMethod(methodName);
//(4) 通过 method1 调用方法: 即通过方法对象来实现调用方法
method1.invoke(o);//传统写法 对象.方法() , 反射机制 方法.invoke(对象)
//输出:Cat say hi!
//(5)java.lang.reflect.Field: 代表类的成员变量, Field 对象表示某个类的成员变量
//得到类中的属性
//Field field = cls.getField("name");//getField不能得到私有的属性
Field field = cls.getField("age");
System.out.println(field.get(o)); // 传统写法:对象.成员变量,反射:成员变量对象.get(对象)
//输出:0
//(6)//java.lang.reflect.Constructor: 代表类的构造方法, Constructor 对象表示构造器
Constructor constructor1 = cls.getDeclaredConstructor(String.class,int.class);//有参构造器
System.out.println(constructor1);
//输出:public reflection.Cat(java.lang.String,int)
Constructor constructor2 = cls.getDeclaredConstructor();//无参构造器
System.out.println(constructor2);
//输出:public reflection.Cat()
}
}
class Cat
{
private String name;
public int age = 0;
public Cat(){}
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
public void hi()
{
System.out.println("Cat say hi!");
}
}
- 反射的优缺点
- 优点:可以动态的创建和使用对象(也是框架底层核心),使用灵活,没有反射机制,框架技术就失去底层支撑;
- 缺点:使用反射基本是解释执行,对执行速度有影响。
- 反射调用优化
- Method和Field,Constructor对象都有
setAccessible()
方法; - setAccessible作用是启动和禁用访问安全检查的开关;
- 参数值为true表示反射的对象在使用时取消访问检查,提高反射的效率;
- 参数值为false则表示反射的对象执行访问检查。
- 代码演示:
- Method和Field,Constructor对象都有
package reflection;
import java.lang.reflect.Method;
public class test {
public static void main(String[] args) throws Exception{
//三种方式访问hi方法效率的比较
m1();
m2();
m3();
//输出:
//传统方法调用耗时:16
//用反射调用耗时:78
//反射优化调用耗时:31
}
//传统调用hi方法
public static void m1()
{
Cat cat = new Cat();
long start = System.currentTimeMillis();
for (int i = 0; i < 900000; i++) {
cat.hi();
}
long end = System.currentTimeMillis();
System.out.println("传统方法调用耗时:"+(end-start));
}
//反射调用hi方法
public static void m2() throws Exception
{
Class cls = Class.forName("reflection.Cat");
Object o = cls.getDeclaredConstructor().newInstance();
Method method = cls.getMethod("hi");
long start = System.currentTimeMillis();
for (int i = 0; i < 900000; i++) {
method.invoke(o);
}
long end = System.currentTimeMillis();
System.out.println("使用反射调用耗时:"+(end-start));
}
//反射调用优化 + 关闭访问检查
public static void m3() throws Exception
{
Class cls = Class.forName("reflection.Cat");
Object o = cls.getDeclaredConstructor().newInstance();
Method method = cls.getMethod("hi");
method.setAccessible(true);//在反射调用方法时,取消访问检查
long start = System.currentTimeMillis();
for (int i = 0; i < 900000; i++) {
method.invoke(o);
}
long end = System.currentTimeMillis();
System.out.println("反射优化调用耗时:"+(end-start));
}
}
class Cat
{
private String name;
public int age = 0;
public Cat(){}
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
public void hi()
{
age++;
}
}
Class类
Class基本介绍
- Class也是类,因此也继承Object类;
- Class类对象不是new出来的,而是系统创建的;
- 对于某个类的Class类对象,在内存中只有一份,因为类只加载一次;
- 每个类的实例都会记得自己是由哪个Class实例所生成;
- 通过Class对象可以完整地得到一个类的完整结构,通过一系列API Class对象是存放在堆的类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据(包括方法代码,变量名,方法名,访问权限等等)。
Class类的常用方法
方法名 功能说明 static Class forName(String name)
(<?> 表示不确定的 Java 类型)返回指定类名name的Class对象 Object getDeclaredConstructor().newInstance() 根据它的参数对该类的构造函数进行搜索并返回对应的构造函数,没有参数就返回该类的无参构造函数,然后再通过newInstance进行实例化。 getName() 返回此Class对象所表示的实体(类、接口、数组类、基本类型等)名称 Class [] getlnterfaces() 获取当前Class对象的接口 ClassLoader getClassLoader() 返回该类的类加载器 Class getSuperclass() 返回表示此Class所表示的实体的超类的Class Constructor[] getConstructors() 返回一个包含某些Constructor对象的数组 Field[] getDeclaredFields() 返回Field对象的一个数组,包含本类中所有属性 Method getMethod(String name.Class … paramTvpes) 返回一个Method对象,此对象的形参类型为paramType 对于:
String classAllPath = "com.reflection.Car";
,有;- 获取到Car类对应的Class对象:
Class<?> cls = Class.forName(classAllPath);
(<?> 表示不确定的 Java 类型) - 得到包名:
System.out.println(cls.getPackage().getName());
- 得到全类名:
System.out.println(cls.getName());
- 通过 cls 创建对象实例:
Car car = (Car) cls.getDeclaredConstructor().newInstance();
- 通过反射获取属性 brand:
Field brand = cls.getField("brand");
- 通过反射给属性赋值:
brand.set(car, "奔驰");
- 得到所有的属性(字段):
Field[] fields = cls.getFields();
- 遍历
Field[] field
数组:for (Field f : fields) {System.out.println(f.getName());}
- 获取到Car类对应的Class对象:
获取Class类对象的6种方式
Class.forName
:应用于通过配置文件获取时;类名.class
:应用场景: 用于参数传递;对象.getClass()
;应用场景,有对象实例;- 通过类加载器来获取到类的 Class 对象:
- 先得到类加载器 car:
ClassLoader classLoader = car.getClass().getClassLoader();
- 通过类加载器得到 Class 对象:
Class cls4 = classLoader.loadClass(classAllPath);
- 先得到类加载器 car:
- 基本数据(
int, char,boolean,float,double,byte,long,short
) 按如下方式得到 Class 类对象:Class<Integer> integerClass = int.class;
Class<Character> characterClass = char.class;
Class<Boolean> booleanClass = boolean.class;
- 基本数据类型对应的包装类,可以通过
.TYPE
得到 Class 类对象:Class<Integer> type1 = Integer.TYPE;
Class<Character> type2 = Character.TYPE;
具有Class对象的类型
- 外部类、成员内部类、静态内部类、局部内部类、匿名内部类
- interfacr:接口;
- 一维数组、二维数组;
- enum:枚举
- annotation:注解;
- 基本数据类型
- void
类加载
基本说明
- 反射机制是java实现动态语言的关键,也就是通过反射实现类动态加载;
- 静态加载:编译时加载相关的类,如果没有则报错,依赖性太强;
- 动态加载:运行时加载需要的类,如果运行时不使用该类,即使不存在该类,也不报错,降低了依赖性。
类加载时机
静态加载
- 当创建对象时(
new
) - 当子类被加载时,父类也加载
- 调用类中的静态成员时
- 当创建对象时(
动态加载:通过反射
Class.forname("reflection.Cat")
类加载过程图
类加载各阶段完成的任务
加载:JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是class文件、也可能是jar包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的java.lang.Class 对象。
连接
- 验证:
- 目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;
- 包括:文件格式验证(是否以魔数oxcafebabe开头)、元数据验证、字节码验证和符号引用验证;
- 可以考虑使用
-Xverify:none
参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间。
- 准备:JVM会在该阶段对静态变量分配内存并默认初始化(对应数据类型的默认初始值,如
0、0L,null,false
等),这些变量所使用的内存都将在方法区中进行分配。 - 解析:虚拟机将常量池内的符号引用替换为直接引用的过程。
- 验证:
初始化
到初始化阶段,才真正开始执行类中定义的Java程序代码,此阶段是执行
clinit()
方法的过程;clinit()
方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并;代码演示:
虚拟机会保证一个类的
clinit()
方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit()
方法,其他线程都需要阻塞等待,直到活动线程执行clinit()
方法完毕。
代码演示:
package demo;
public class test {
public static void main(String[] args) throws ClassNotFoundException {
//1. 加载 B 类,并生成 B 的 class 对象
//2. 链接 num = 0
//3. 初始化阶段
// 依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并合并
/*
clinit() {
System.out.println("B 静态代码块被执行");
//num = 300;
num = 100;
}
合并: num = 100
*/
//new B();//类加载
//System.out.println(B.num);//100, 如果直接使用类的静态属性,也会导致类的加载
//看看加载类的时候,是有同步机制控制
/*
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
//正因为有这个机制,才能保证某个类在内存中, 只有一份 Class 对象
synchronized (getClassLoadingLock(name)) {
//....
}
}
*/
B b = new B();
}
}
class B {
static {
System.out.println("B 静态代码块被执行");
num = 300;
}
static int num = 100;
public B() {//构造器
System.out.println("B() 构造器被执行");
}
}
通过反射获取类的结构信息
- 第一组:
java.lang.Class
类getName
:获取全类名getSimpleName
:获取简单类名getFields
:获取所有public修饰的属性,包含本类以及父类的getDeclaredFields
:获取本类中所有属性getMethods
:获取所有public修饰的方法,包含本类以及父类的getDeclaredMethods
:获取本类中所有方法getConstructors
:获取本类所有public修饰的构造器getDeclaredConstructors
:获取本类中所有构造器getPackage
:以Package形式返回包信息getSuperClass
:以Class形式返回父类信息getInterfaces
:以Class[]形式返回接口信息getAnnotations
:以Annotation[] 形式返回注解信息
- 第二组:
java.lang.reflect.Field
类getModifiers
:以int形式返回修饰符
[说明:默认修饰符是0,public是1,private是2,protected是4,static是8,final是16]getType
:以Class形式返回类型getName
:返回属性名
- 第三组:
java.lang.reflect.Method
类getModifiers
:以int式返回修饰符
[说明:默认修饰符是0,public是1,private是2,protected是4,static 是8,final是16]getReturnType
:以Class形式获取 返回类型getName
:返回方法名getParameterTypes
:以Class[]返回参数类型数组
- 第四组:
java.lang.reflect.Constructor
类getModifiers
:以int形式返回修饰符getName
:返回构造器名(全类名)getParameterTypes
:以Class[]返回参数类型数组
通过反射创建对象
通过public的无参构造器创建实例:
getDeclaredConstructor().newInstance();
通过public的有参构造器创建实例:
- 先得到对应的构造器:
Constructor<?> constructor = cls.getConstructor(String.class);
- 再创建实例并传入参数:
Object o2 = constructor.newInstance("Jack");
- 先得到对应的构造器:
通过非public的有参构造器创建实例:
- 先得到 private 的构造器对象:
Constructor<?> constructor2 = cls.getDeclaredConstructor(String.class,int.class);
- 再取消访问检查:
constructor2.setAccessible(true);
- 最后创建对象:
Object o3 = constructor2.newInstance("rose",18);
- 先得到 private 的构造器对象:
代码演示:
package reflection;
import java.lang.reflect.Constructor;
public class test {
public static void main(String[] args) throws Exception{
//先获取Cat类的Class对象
Class<?> cls = Class.forName("reflection.Cat");
//1.通过public的无参构造器创建实例
Object o = cls.getDeclaredConstructor().newInstance();
System.out.println(o);
//输出:Cat [age=0, name=null]
//2.通过public的有参构造器创建实例
//得到对应构造器
Constructor<?> constructor = cls.getConstructor(String.class);
//创建实例并传入参数
Object o2 = constructor.newInstance("Jack");
System.out.println(o2);
//输出:Cat [age=0, name=Jack]
//3.通过非 public 的有参构造器创建实例
// 得到 private 的构造器对象
Constructor<?> constructor2 = cls.getDeclaredConstructor(String.class,int.class);
//创建实例
//暴破【暴力破解】,使用反射可以访问 private 构造器/方法/属性
constructor2.setAccessible(true);
Object o3 = constructor2.newInstance("rose",18);
System.out.println(o3);
//输出:Cat [age=18, name=rose]
}
}
class Cat
{
private String name;
public int age = 0;
public Cat(){}
public Cat(String name)
{
this.name = name;
}
private Cat(String name, int age) {
this.name = name;
this.age = age;
}
public void hi()
{
age++;
}
public String toString() {
return "Cat [age=" + age + ", name=" + name + "]";
}
}
通过反射访问类中的成员
- 访问属性
- 根据属性名获取Field对象
Field f = clazz对象.getDeclaredField(属性名);
- 爆破:
f.setAccessible(true);
(f 是Field) - 访问:
f.set(o,值)
:o表示对象System.out.println(f.get(o));
:o表示对象
- 注意:如果是静态属性,则set和get中的参数o,可以写成null
- 根据属性名获取Field对象
- 访问方法
- 根据方法名和参数列表获取Method方法对象:
Method m =clazz.getDeclaredMethod(方法名,XX.class);
//得到本类的所有方法 - 获取对象:
Object o=clazz.newlnstance();
- 暴破:
m.setAccessible(true);
- 访问:
Object returnValue = m.invoke(o,实参列表);
//o就是对象 - 注意:如果是静态方法,则invoke的参数o,可以写成null
- 根据方法名和参数列表获取Method方法对象: