集合框架概述
1.1为什么使用集合框架
我们在java中学习过一个数据类型 --- 数组
数组存在问题: 定容 (数组初始化的时候 一旦固定容量 从此就只能存储该容量的数据)
如果我们一旦创建数组 发现数组的容量不够用了 此时就需要扩容就非常麻烦了。
所以我们自己写了一个类MyArray
在MyArray中定义方法getData()和setData(代码在下方)
此时我们可以手撕定义数组Java官方也能,所以 java官方就基于数组,
根据不同的数据结构封装出来了很多的类,
这些类统称为集合框架
MyArray.java
import java.util.Arrays;
public class MyArray {
Object[] arr;
int size=0;//获得数组的小标
/*带参构造 自定义数组长度*/
public MyArray(int size) {
if (size<0){
throw new RuntimeException("数组长度不合法");
}
arr = new Object[size];
}
/*setData()是为了设置数据*/
public void setData(Object o){
if (size>= arr.length){
/*如果还想设置数据 但数据下标不够就扩容 使用该方法使得新的数组的长度变为原来的2倍*/
Object[] newArr = Arrays.copyOf(arr, arr.length * 2);
arr = newArr;
}
/*设置数据*/
arr[size] = o;
/*数组下标要自增*/
size++;
}
/*getData()是为了获得数据*/
public Object getData(int index){
/*如果想得到某个数据而数组下标有误抛出这个异常*/
if(index>=arr.length||index<0){
throw new ArrayIndexOutOfBoundsException("数组下标传入有误");
}
/*否则就将该值传给o返回给出去*/
Object o = arr[index];
return o;
}
}
测试类
public class Test {
public static void main(String[] args) {
MyArray m = new MyArray(3);
m.setData("java01");
m.setData("java02");
m.setData("java03");
m.setData("java04");
System.out.println(m.getData(0));
System.out.println("数组的长度为:"+m.arr.length);
for (Object a:m.arr){
System.out.println(a);
}
}
}
1.2集合框架体系
1.2.1 概念
Java集合框架(Java Collections Framework简称JCF)是为表示和操作集合,而规定的一种统一的标准的体系结构。集合框架包含三大块内容:对外的接口、接口的实现和对集合运算的算法。集合就是用于存储对象的容器。 只要是对象类型就可以存进集合框架中。集合的长度是可变的。 集合中不可以存储基本数据类型的值
1.2.2 集合和数组的区别
数组和集合相比数组的缺点是它长度是固定的,没有办法动态扩展。 而集合存储数据时是没有长度限制的,是可以动态扩展的。集合容器因为内部的数据结构不同,有多种不同的容器对象。这些容器对象不断的向上抽取,就形成了集合框架。
1.3 List接口及实现类
1.3.1 特点
(1)List集合是有序集合: 数据的添加和存储次序一致
(2)List集合可以存储重复的数据
(3)List集合中的数据可以通过下标访问
List集合中用到的方法 如下图(所有方法下面代码都会实现)
1.3.2 ArrayList实现类特点
- 实现了List接口
- 可以动态扩容(我们只管存,长度不够,底层会自动的扩容)
- 通过下标可以快速访问数据
- 查找快,插入删除慢
- ArrayList底层是数组,对数组做了封装
- 可以存储任意类型的数据,包括null
- 数据按照存储次序排列
- 数据可以重复
- 多线程访问时不安全
1.3.3 ArrayList的创建和使用
1.3.3.1创建集合对象
List list = new ArrayList(); //创建一个集合对象 如果没有指定集合容器的长度默认为10
List list1 = new ArrayList(15);
1.3.3.2 添加元素
/*添加元素操作,插入的值可以是任意类型*/
list1.add("java01");
list1.add("java02");
list1.add(15.5);
list1.add(18);
list1.add(true);
list1.add(new Date());
list1.add(2,"hello");
System.out.println(list1);
list2.add("a");
list2.addAll(list1);//将list1中全部元素添加到list2中
System.out.println(list2);
1.3.3.2 删除元素
list1.remove(2);//移除下标为2的元素
System.out.println(list1);
list1.clear();//清空集合中的元素.
System.out.println(list1);
1.3.3.2 修改元素
list1.set(1,"刘德华");//将下标为1设置为刘德华
System.out.println(list1);
1.3.3.2 查找操作
/*查询操作*/
Object o = list1.get(0);//根据下标获取元素
System.out.println(o);
int size = list1.size();//获得集合中元素的个数
System.out.println(size);
boolean f = list1.contains("java01");//判断集合中是否存在某个元素
System.out.println(f);
System.out.println(list1);
int index = list1.indexOf("java02");//存在就返回第一次出现的位置否则返回-1
System.out.println(index);
/*遍历循环集合中的元素*/
for (Object o2:list1){
System.out.println(o2);
}
1.3.4 ArrayList底层源码
从构造方法来入手
new ArrayList(22) 底层声明了一个Object类型的数组 名字elementData
Object[] elementData
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) { //大于0
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) { //等于初始化为一个空数组
this.elementData = EMPTY_ELEMENTDATA;
} else { //抛出一个异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
==========add("java01")======E理解为Object类型================
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 扩容
elementData[size++] = e; //把元素赋值给数组的相应位置
return true;
}
==========indexOf("java02") 判断元素在集合中第一次的位置=============
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i])) //和数组中的每个元素内容进行比对
return i; //返回元素在集合中位置
}
return -1;
}
===========size() 请求数组的长度======================
public int size() {
return size;
}
============contain("java05")判断元素是否在集合中==============
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
===============get(1) 获取指定位置的元素========
public E get(int index) {
rangeCheck(index); //判断指定的位置是否合法
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
============toString() 为什么不打印对象的引用地址
[java01, java02, java03, java02]因为重写了Object里面的toString方法。
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
通过对ArrayList方法的底层代码分析:底层就是对数组的操作。
ArrayList的底层就是基于数组实现的。
1.4 LinkedList集合
它是一个链表结构。
LinkedList: 具有List的特征,底层以链表结构实现,
可以进行头尾元素的添加删除;
1.4.1 LinkedList与ArrayList的区别
ArrayList 底层体现出来的数据结构是 数组
Linkedlist 底层体现出来的数据结构是 链表
1.4.2 LinkedList的使用
1.4.2.1 添加操作
LinkedList linkedList = new LinkedList();
linkedList.add("java01");
linkedList.addFirst("java02"); //添加到头部
linkedList.addLast("java03");//追加到尾部
linkedList.addFirst("java04"); //追加到头部
linkedList.addLast("java05");//追加到尾部
System.out.println(linkedList);
1.4.2.1 删除操作
linkedList.removeFirst();//移除头部元素
System.out.println(linkedList);
linkedList.remove(2);//移除指定位置的元素
System.out.println(linkedList);
linkedList.removeLast();//移除尾部的元素
System.out.println(linkedList);
1.4.2.1 修改操作
linkedList.set(1,"java11");
1.4.2.1 查询操作
//查询操作
int size = linkedList.size();//求长度
boolean empty = linkedList.isEmpty();//是否为空
boolean b = linkedList.contains("java01");//判断元素是否在集合中
Object o = linkedList.get(1);//根据下标获取指定位置的元素
Object first = linkedList.getFirst();//获取第一个元素
System.out.println(first);
Object last = linkedList.getLast();
System.out.println(last);
1.4.3 LinkedList底层源码
1.凡是查询源码 ,我们都是从类的构造方法入手:
/**
* Constructs an empty list.
*/
public LinkedList() {
}
该类的构造方法内是空的,没有任何的代码。 但是该类中有三个属性。
transient int size = 0; //索引
transient Node<E> first; //第一个元素对象
transient Node<E> last; //表示最后一个元素对象。
================ add的源码=====E:理解为Object类型==========================。
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
//上一个节点 数据 下一个节点
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
==================Node的源码 内部类=======================================
private static class Node<E> { //<E>泛型--object
E item; //数据
Node<E> next; //下一个节点
Node<E> prev; //上一个节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
==================== get(1)-----获取元素========================
public E get(int index) {
checkElementIndex(index); //检查index下标是否正确。
return node(index).item; //李四Node对象
}
========================node(index)=============================
Node<E> node(int index) {
//>> 位运算二进制运算 ----- size >> 1 一半的意思size/2
if (index < (size >> 1)) { //前半部分
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else { //后半部分
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
1.4.4 总结LinkedList和ArrayList
List:它是一个接口,该集合中的元素可以重复,而且是有序的(有下标)。
ArrayList--->它的底层是数组结构,它的查询效率高,但是它的添加和删除效率低
(因为它要牵涉到数据的迁移。)
常见方法: add();size();indexOf(); contains(); get();remove();isEmpty();
LinkedList--->底层双向链表结构,它增加和删除效率高,但是它的查询效率低。
常见的方法: 它的方法和ArrayList中的方法比较相似,它也有自己特有的方法:
addFirst() addLast() getFirst() getLast()
2.1 Set接口及实现类
2.1.1 Set接口特点
- Set接口是无序的
- Set接口中的数据不允许重复
- Set接口无法通过下标访问数据
- 查找慢,插入删除快(底层数据结构是哈希表和红黑树)
- Set集合使用equals()和hashCode()方法实现元素去重
2.1.2 HashSet实现类
2.1.2.1HashSet特点:
- HashSet是Set接口的实现类
- 线程不安全
2.1.2.2 创建HashSet对象
public class Test02 {
public static void main(String[] args) {
//如果没有指定容器大小 默认大小是16 装在因子是0.75
HashSet hashSet= new HashSet();
HashSet hashSet1 = new HashSet(16);//初始容器的大小(默认也是16)
//loadFactor:--->0.7f 表示负载因子 当空间使用70%时 要求扩容
HashSet hashSet2 = new HashSet(16,0.7f);
}
}
2.1.2.3 添加元素
HashSet hashSet = new HashSet();
HashSet hashSet1 = new HashSet(16);
hashSet.add("java01");
hashSet.add("java02");
hashSet.add("java03");
hashSet.add("java04");
hashSet.add("java02"); //无序不重复所以加不进去 hashet中元素只有4个
System.out.println(hashSet);
hashSet1.add("刘德华");
hashSet1.add("张学友");
hashSet1.add("黎明");
hashSet.addAll(hashSet1);//添加多个元素
System.out.println(hashSet);
2.1.2.3 删除元素
System.out.println(hashSet);
hashSet.remove("黎明");
System.out.println(hashSet);
hashSet.clear();//清空元素
System.out.println(hashSet);
2.1.2.4 查询元素
boolean c = hashSet.contains("刘德华");
System.out.println(c);
boolean empty = hashSet.isEmpty();
System.out.println(empty);
2.1.2.5 遍历元素
1.foreach遍历
/*foreach遍历*/
for (Object o:hashSet) {
System.out.println(o);
}
2.迭代器遍历(Iterator)
迭代器(iterator)有时又称游标(cursor)是程序设计的软件设计模式,
可在容器对象container,例如链表或数组)上遍访的接口,
设计人员无需关心容器对象的内存分配的实现细节。
提供的方法:
hasNext():判断指针是否能够移动,能移动返回true否则返回false
next():指针移动并获取指定所在位置的元素
Iterator it = hashSet.iterator();
while (it.hasNext()){
Object next = it.next();
System.out.println(next);
}
2.1.2.6 HashSet源码
从构造函数说起:
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
在创建一个HashSet的对象时,底层创建的是HashMap。HashMap底层后续更新
2.2 TreeSet实现类
TreeSet中的方法和HashSet中的方法一模一样 只是他们的实现不一样。
TreeSet 基于TreeMap 实现。TreeSet可以实现有序集合,但是有序性需要通过比较器实现。
2.2.1 特点
- 有序
- 不重复添加、删除、
- 判断元素存在性效率比较高
- 线程不安全
2.2.2 TreeSet操作
1.存储String类型的元素
TreeSet treeSet = new TreeSet();
treeSet.add("java01");
treeSet.add("java02");
treeSet.add("java03");
System.out.println(treeSet);
2.存储一个对象类型
treeSet.add(new Student("张三",18));
treeSet.add(new Student("李四",17));
treeSet.add(new Student("王五",19));
System.out.println(treeSet);
此时编译器报错
解决办法有两个:
第一个: 让Student类实现Comparable接口
//Student类
public class Student implements Comparable{
String name;
Integer age;
public Student(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public Integer getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return
"name='" + name + '\'' +
", age=" + age;
}
/*排序:---返回如果大于0 表示当前元素比o大 如果返回-1 当前添加的元素比o小 返回0表示相同元素。*/
@Override
public int compareTo(Object o) {
Student st = (Student) o;
if(this.age>st.age){
return 1;
}else if (this.age < st.age){
return -1;
}
return 0;
}
}
//测试类
public class TreeSetDemo {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet();
treeSet.add(new Student("张三",18));
treeSet.add(new Student("李四",17));
treeSet.add(new Student("王五",19));
System.out.println(treeSet);
}
}
第二种: 在创建TreeSet时指定排序的对象。
这种方法主要用到TreeSet集合中的带参构造
底层原理代码是:
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
所以要实现Comparator接口,因此我们可以自定义类实现Comparator接口即可,具体代码如下:
Student类:
public class Student{
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public Integer getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return
"name='" + name + '\'' +
", age=" + age;
}
}
自定义类实现Comparator接口
public class MyComparator implements Comparator{
@Override
//原理与ComparableTo那个相同
public int compare(Object o1, Object o2) {
Student s1 = (Student)o1;
Student s2 = (Student) o2;
if (s1.getAge()>s2.getAge()){
return 1;
}else if(s1.getAge()<s2.getAge()){
return -1;
}
return 0;
}
}
测试类:
public class TreeSetDemo {
public static void main(String[] args) {
MyComparator myComparator = new MyComparator();
TreeSet treeSet = new TreeSet(myComparator);
treeSet.add(new Student("张三",18));
treeSet.add(new Student("李四",17));
treeSet.add(new Student("王五",19));
treeSet.add(new Student("赵六",20));
System.out.println(treeSet);
}
}
注意:如果运行不起来记得看看导包是否错误
3.1 Map接口极其实现类
Map接口特点:
- 以键值对方式存储数据(Collection是单值集合)
- 键不能重复,键重复时,后面的数据会覆盖前面的数据
- 可以存储null键值对
- 数据无序
3.1.1 HashMap实现类
HashMap实现了Map接口,拥有Map接口的基本特点。HashMap线程不安全,效率高。HashMap的底层是由哈希表、链表加红黑树构成的
3.1.1.1 创建HashMap对象
//默认初始化大小16 负载因子0.75
Map hashMap1 = new HashMap();
Map hashMap2 = new HashMap(16);
Map hashMap3 = new HashMap(16,0.75f);
2.3.1.2 添加操作
/*添加操作*/
//存储按键值对
hashMap1.put("张三",18);
hashMap1.put("李四",19);
hashMap1.put("王五",20);
//键不能重复 否则覆盖值
hashMap1.put("王五",21);
System.out.println(hashMap1);
//添加多个键值对
hashMap2.putAll(hashMap1);
System.out.println(hashMap2);
/*hashMap1.putIfAbsent():如果指定的key存在,则不放入hashmap1中 如果不存在则放入*/
hashMap1.putIfAbsent("赵六",20);
hashMap1.putIfAbsent("赵六",21);
System.out.println(hashMap1);
2.3.1.3 删除操作
/*删除操作*/
hashMap1.remove("王五");
System.out.println(hashMap1);
hashMap2.clear();//清空map容器
System.out.println(hashMap2);
2.3.1.4 修改操作
hashMap1.replace("李四",17);
System.out.println(hashMap1);
2.3.1.5 查询操作
/*查询操作*/
hashMap1.put("王五",19);
System.out.println(hashMap1);
boolean f = hashMap1.containsKey("张三");//是否包含指定的key
System.out.println(f);
Object v = hashMap1.get("张三");//根据key获得value
System.out.println(v);
Set set = hashMap1.keySet();//获得key集合
System.out.println(set);
//遍历 根据key集合获得其值
for (Object key:set) {
Object value = hashMap1.get(key);
System.out.println(key+"====>"+value);
}
3.1.2 HashMap的底层原理
JDK1.7 和 JDK1.8他们是有区别得。
JDK1.7使用得数据结构: 数组+链表 而且链表插入模式为头部插入(造成死循环)。
jdk1.8使用得数据结构: 数组+链表+红黑树 而且链表得插入模式为尾部插入。
从构造函数入口:
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
JDK1.8 HashMap原理
存储元素使用put(key,value),根据key的hash计算出相应得哈希值,根据相应的算法求出该元素在数组中的位置, 如果求出的哈希值相同,则称为哈希冲突,会根据equals()来判断元素是否一致,如果equals不同,则挂在单向链表上(就是数据结构中的链地址法解决冲突), 如果哈希碰撞得个数超过8个,则把链表转换为红黑二叉树。
//底层代码如下
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//如果key得hash值相同,判断key得equals是否相同,替换原来得元素
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 判断链表得长度是否超过8个
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 把链表转换为红黑树结构
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
3.1.2 Hashtable实现类
Hashtable也实现了Map接口,但是与HashMap相比有如下特点:
以键值对方式存储数据
键不能重复
数据无序键和值都不能存储null
线程安全,效率低
Hashtable hashtable =new Hashtable();hashtable.put("aaa",100);//hashtable.put("aaa",null);hashtable.put(null,100);
hashtable.put("xxx",100);
System.out.println(hashtable);
4.1 泛型
1. 泛型就是限制我们得数据类型。
2.为什么使用泛型?
我们原来在定义集合时,是如下得定义方式:
List list=new ArrayList();//该集合没有使用泛型
list.add("java01");
list.add("java02");
String str= (String) list.get(0);//获取元素 需要进行强制类型转换
System.out.println(str);
获取元素时,不方便对元素进行相应得其他操作。
3.使用泛型
List<类型> list=new ArrayList<类型>(); 只能在该集合中存储指定得类型。
public static void main(String[] args) {
List<String> list=new ArrayList<>();//这里就限制了集合中每个元素得类型。
list.add("java01");
list.add("hello"); //因为集合中只能添加String类型
list.add("world"); //因为集合中只能添加String类型
String s = list.get(0); //在获取元素时 默认就是相应得数据类型 而无需在进行转换
//<K,V>:K:表示键得泛型 V:表示值得泛型
HashMap<String,Integer> map=new HashMap<>();
map.put("name",15);
map.put("age",25);
Set<String> keys = map.keySet();
}
4.自定义泛型
public class Test03 {
public static void main(String[] args) {
//在创建泛型类对象时没有指定相应得泛型类型。则默认为Object类型
Point p1=new Point();
p1.setX(15);
p1.setX("北纬50度");
//这里得泛型必须都是对象类型
Point<Integer> p2=new Point<Integer>() ;
p2.setX(25);
p2.setY(65);
Integer x = p2.getX();
Integer y = p2.getY();
p2.show();
Point<String> p3=new Point<>();
}
}
class Point<T>{
private T x;
private T y;
public void show(){
System.out.println("x坐标为:"+x+";y坐标为:"+y);
}
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
public T getY() {
return y;
}
public void setY(T y) {
this.y = y;
}
}
5.1 总结
1) 需要数据唯一时,使用Set集合
- 需要保持规则顺序,此时用TreeSet
- 不需要指定顺序,此时用HashSet
- 需要保证代码添加数据的次序,此时用LinkedHashSet
2) 不需要唯一时,使用List集合
- 需要频繁增删操作时,使用LinkedList
- 不需要频繁增删操作,需要做大量查询操作时,使用ArrayList
3) 如果以键值对方式存储数据时,使用HashMap
4) 通过类名记住集合框架类的结构和所属体系
List: ArrayList LinkedList
Set : HashSet TreeSet
-
后缀名就是该集合所属的体系。
-
前缀名就是该集合所属的数据结构。
-
看到array:想到的就是数组,查询速度快。因为有角标是连续的内存地址。
-
看到link:想到的就是链表,增删动作的速度快,而且要想到 add get remove+first /last的 方法
-
看到hash:想到的就是哈希表,那么就具有唯一性,
而且要想到元素需要覆盖hashcode方法 和equals方法 -
看到tree:想到的就是二叉树,接下来想到的就是排序,然后就是两个接口 Comparable接口
实现compareTo(To) Comparator接口 实现compare() 方法