什么是集合框架?
Java 集合就像一种容器,可以把多个对象的引用放入容器中。
集合的常见面试题
- ArrayList和LinkedList的区别?
- List和Set区别?
- hashSet和List区别?
- hashMap和treeMap区别?
数组和集合框架的区别
- 数组只能存储相同类型的数据,比如int[] arr = new int[10];只能保存整数;集合可以存储不同类型的数据。
- 数组可以保存基本数据类型的数据,也可以保存引用数据类型的数据;集合框架只能保存对象(集合框架里面的数据都是对象)。
- 数组的长度一旦定义,是不可改变的。比如Student[] stu = new Student[10];这个数组只能保存10个学生信息,这也是数组的弊端;集合可以存储数量不确定的数据。
集合框架的分支
主要分为两个接口
- Collection接口:单列集合,存储一个个对象。
- List接口:存储有序的、可重复的数据,可以看作动态数组,有下标。
三个实现类:ArrayList、LinkedList、Vector。 - Set接口:存储无序的、不可重复的数据。
三个实现类:HashSet、LinkedHashSet、TreeSet。
- List接口:存储有序的、可重复的数据,可以看作动态数组,有下标。
- Map接口:多列集合,存储键值对(key-value)。
实现类:HashMap、LinkedHashMap、TreeMap、Hashtable、Properties。
1.Collection接口
Collection接口时List接口和Set接口的父接口,它定义了一些方法供List接口和Set接口使用。
第一波方法👇。
//创建一个集合,用ArrayList举例它的方法。
Collection coll = new ArrayList();
//1.size():返回集合中元素的个数(集合的长度)
System.out.println(coll.size());//0
//2.add(Object o):添加元素 掌握
coll.add(1);//添加的1不是int类型,而是int的包装类Integer,因为集合保存的都是对象
coll.add("TalentO_o");
coll.add(10.1);
coll.add(new Date());
coll.add("abc");
coll.add("abc");
System.out.println(coll.size());//6
System.out.println(coll);//[1, TalentO_o, 10.1, Wed Feb 24 20:09:28 CST 2021, abc, abc]
//3.addAll(Collection c):把集合c里面所有的元素添加到当前集合中
Collection c = new ArrayList();
c.add("aaa");
c.add("bbb");
coll.addAll(c);
System.out.println(coll.size());//8
System.out.println(coll);//[1, TalentO_o, 10.1, Wed Feb 24 20:09:28 CST 2021, abc, abc, aaa, bbb]
//4.isEmpty():判断当前集合是否为空,如果为空返回true 掌握
System.out.println(coll.isEmpty());//false
//5.clear():清空集合中的元素
coll.clear();
System.out.println(coll.isEmpty());//true
第二波方法👇。
Person类👇。
package com.hpe.collection;
import java.util.Objects;
/**
* @ClassName Person
* @Description 添加描述
* @Author Waynejwei
* @LastChangeDate 2021/2/22 11:54
* @Version v1.0
*/
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
Collection coll = new ArrayList();
coll.add(123);
coll.add("aaa");
coll.add(new String("bbb"));
coll.add(new Person("张三", 12));
//6.contains(Object o):判断当前集合中是否包含指定元素o 掌握
boolean b1 = coll.contains(123);
System.out.println(b1);//true
//判断的时候会调用o对象所在类的equals方法
//Srting重写了equals方法,重写之后比较的是字符串内容
boolean b2 = coll.contains(new String("bbb"));
System.out.println(b2);//true
//如果我重写了Person的equals方法,比较的是实体内容,b3=true
//如果我没有重写equals方法,比较的是地址值,b3=flase
boolean b3 = coll.contains(new Person("张三", 12));
System.out.println(b3);//false
//7.containsAll(Collection c):判断集合c中多有的元素是否都存在于当前集合中
Collection coll_ = new ArrayList();
coll_.add(123);
coll_.add(new String("bbb"));
System.out.println(coll.containsAll(coll_));//true
//8.remove(Object o):删除集合中的元素o 掌握
boolean b4 = coll.remove(123);
System.out.println(b4);//true
//9.removeAll(Collection c):从当前集合中删除另一个集合c所有的元素
Collection c = new ArrayList();
c.add("aaa");
c.add("abc");
System.out.println(coll.removeAll(c));//只要有一个删除成功就返回true,表示可以删除
System.out.println(coll);
//10.toArray():把集合转成数组 重要
//转成数组之后就可以使用下标值
Object[] objs = c.toArray();
for (Object o : objs) {
System.out.println(o);
}
//11.equals():判断两个集合的元素值是否相等
//12.hashCode():返回地是当前对象的哈希值 重要
System.out.println(coll.hashCode());
Collection接口的迭代方式。
Collection coll = new ArrayList();
coll.add(1);
coll.add("TalentO_o");
coll.add(10.1);
coll.add(new Date());
coll.add("abc");
coll.add("abc");
-
迭代器 Iterator,两个方法:hashNext()和next()。
Iterator i = coll.iterator();//返回一个iterator接口
//第一种方式
//System.out.println(i.next());
//System.out.println(i.next());
//System.out.println(i.next());
//System.out.println(i.next());
//System.out.println(i.next());
//System.out.println(i.next());//hashNext():判断当前集合是否有下一个元素 while (i.hasNext()) { //next():1.指针下移 2.输出当前元素 System.out.println(i.next()); }
-
增强for循环。
for (Object o : coll){
System.out.println(o);
} -
把集合转成数组,遍历数组。这个方法是要是用于Set接口,因为List接口是有序的,它的集合本身就可以使用下标,而Set接口无序,不能使用下标,所以要先转成数组,但这种方法不提倡使用。
//toArray()返回地是对象类型的数组
Object[] objects = coll.toArray();
for (Object o : objects){
System.out.println(o);
}//补充一个把数组转成集合的方法 List list = Arrays.asList(new String[]{"aaa","bbb"}); for(Object str : list){ System.out.println(str); }
2.List接口
List接口是Collection接口的一个子接口,存储有序的、可重复的数据,集合中的每个元素都有其对应的顺序索引,List 默认按元素的添加顺序设置元素的索引。
它有三个主要实现类:ArrayList、LinkedList、Vector。其中最常用的是ArrayList。
List接口也有一些独有的方法,首先来介绍一下这些方法。
List list = new ArrayList();
list.add("aaa");
list.add("bbb");
list.add(123);
list.add("bbb");
System.out.println(list);//[aaa, bbb, 123, bbb]
//1.void add(int index, Object ele):根据index,把元素添加到指定位置
list.add(0, 110);
System.out.println(list);//[110, aaa, bbb, 123, bbb]
//2.boolean addAll(int index, Collection eles):根据index,把集合添加到指定位置
//3.Object get(int index):根据index查找对应元素
System.out.println(list.get(2));//bbb
//4.int indexOf(Object obj):查询元素obj第一次出现的索引值,如果找不到返回-1
System.out.println(list.indexOf("bbb"));//1
//5.int lastIndexOf(Object obj):查询元素obj最后一次出现的索引值,如果找不到返回-1
System.out.println(list.lastIndexOf("bbb"));
//6.Object remove(int index):根据索引值删除该元素
list.remove(0);
//7.Object set(int index, Object ele):根据索引值修改元素
list.set(0, "hehe");
System.out.println(list);
//最常用的就是 增删改查、size()
遍历List的方式(三种)。
List list = new ArrayList();
list.add("aaa");
list.add("bbb");
list.add(123);
list.add("bbb");
System.out.println("====================迭代器");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
System.out.println("====================增强for");
for (Object o : list) {
System.out.println(o);
}
System.out.println("====================普通for");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
//System.out.println("=====================转成数组,建议不这么写");
2.1.ArrayList实现类
它是重要实现类,底层就是数组 Object[] elementData,线程不安全,查询速度比较快,插入和删除速度慢。
分析一下jdk8中ArrayList类的源码。
//默认初始化长度是10
private static final int DEFAULT_CAPACITY = 10;
//空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//ArrayList底层使用的就是这个数组
transient Object[] elementData; // non-private to simplify nested class access
//当传入的初始容量initialCapacity > 0为真时,创建一个大小为initialCapacity的空数组,
//并将引用赋给elementData;
//当传入的初始容量initialCapacity = 0为真时,将空数组EMPTY_ELEMENTDATA赋给elementData;
//当传入的初始容量initialCapacity < 0为真时,直接抛出IllegalArgumentException异常。
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
}
}
//无参构造时,Obeject数组elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
//但没有指定长度,而是在add方法进行了长度初始化。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//这里只需要知道的一个就是如果传入结合为空,那么将空数组EMPTY_ELEMENTDATA赋给elementData
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
//add:添加元素
public boolean add(E e) {
//在这里调用了ensureCapacityInternal方法
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
2.2.LinkedList实现类
底层是使用双向链表的形式进行存储的,增删速度比较快,查询速度慢。
源码分析👇。
容量
transient int size = 0;
//需要注意的是头结点前驱指针为null,尾节点后驱指针为null。
//指向首(头)节点
transient Node<E> first;
//指向尾节点
transient Node<E> last;
//构建一个空列表
public LinkedList() {
}
构建一个包含集合c的列表
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
//将节点值为e的节点作为首节点
private void linkFirst(E e) {
//得到首节点
//如果原来是空链表,f和first都指向null
final Node<E> f = first;
//构建一个prev值为null,next为f,节点值为e的新节点
//如果原本是个空链表,那么newNode的前驱和后继都指向null
//如果原来不是空链表,那么newNode的前驱为null,后继为原来的first。
final Node<E> newNode = new Node<>(null, e, f);
//设置首节点,让first指向新节点
first = newNode;
//如果f指向null,说明原本是个空链表,newNode是第一个节点,那么也是最后一个节点
if (f == null)
//让last指向newNode
last = newNode;
else
//如果f不指向null,说明原来链表不为空,那么原来的first的前驱要指向新节点
f.prev = newNode;
//长度增加
size++;
//modCount是用来记录修改次数,由于LinkedList是非线程安全的,任何对LInkedList的修改都会modCount++操作。迭代器迭代初始化时会将modCount赋值给 expectedModCount。在迭代过程中,会判断 modCount是否等于expectedModCount,如果不等,说明其他线程修改了LinkedList,就会抛出ConcurrentModificationException异常。
modCount++;
}
//使用对应参数作为尾节点
void linkLast(E e) {
//得到尾节点
//如果原来是空链表,l和last都指向null
final Node<E> l = last;
//构建一个prev值为l,next为null,节点值为e的新节点
//如果原本是个空链表,那么newNode的前驱和后继都指向null
//如果原来不是空链表,那么newNode的前驱为l,后继为原来的first。
final Node<E> newNode = new Node<>(l, e, null);
设置尾节点,让last指向新节点
last = newNode;
//如果l指向null,说明原本是个空链表,newNode是第一个节点,那么也是头节点
if (l == null)
first = newNode;
else
//如果l不指向null,说明原来链表不为空,那么原来的last的后继要指向新节点
l.next = newNode;
size++;
modCount++;
}
//在指定节点前插入节点,节点succ不能为空
void linkBefore(E e, Node<E> succ) {
final Node<E> pred = succ.prev;//获取前一个节点
//使用参数创建新的节点,向前指向前一个节点,向后指向当前节点
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;//当前节点指向新的节点
if (pred == null)
first = newNode;//如果前一个节点为null,新的节点就是首节点
else
pred.next = newNode;//如果存在前节点,那么前节点的向后指向新节点
size++;
modCount++;
}
//删除首节点并返回删除前首节点的值,内部使用
private E unlinkFirst(Node<E> f) {
final E element = f.item;//获取首节点的值
final Node<E> next = f.next;//得到下一个节点
f.item = null;
f.next = null; //便于垃圾回收期清理
first = next; //首节点的下一个节点成为新的首节点
if (next == null)
last = null; //如果不存在下一个节点,则首尾都为null(空表)
else
next.prev = null;//如果存在下一个节点,那它向前指向null
size--;
modCount++;
return element;
}
//删除尾节点并返回删除前尾节点的值,内部使用
private E unlinkLast(Node<E> l) {
final E element = l.item;//获取值
final Node<E> prev = l.prev;//获取尾节点前一个节点
l.item = null;
l.prev = null; //便于垃圾回收期清理
last = prev; //前一个节点成为新的尾节点
if (prev == null)
first = null; //如果前一个节点不存在,则首尾都为null(空表)
else
prev.next = null;//如果前一个节点存在,先后指向null
size--;
modCount++;
return element;
}
//删除指定节点并返回被删除的元素值
E unlink(Node<E> x) {
//获取当前值和前后节点
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next; //如果前一个节点为空(如当前节点为首节点),后一个节点成为新的首节点
} else {
prev.next = next;//如果前一个节点不为空,那么他先后指向当前的下一个节点
x.prev = null; //方便gc回收
}
if (next == null) {
last = prev; //如果后一个节点为空(如当前节点为尾节点),当前节点前一个成为新的尾节点
} else {
next.prev = prev;//如果后一个节点不为空,后一个节点向前指向当前的前一个节点
x.next = null; //方便gc回收
}
x.item = null; //方便gc回收
size--;
modCount++;
return element;
}
//获取第一个元素
public E getFirst() {
final Node<E> f = first;//得到首节点
if (f == null) //如果为空,抛出异常
throw new NoSuchElementException();
return f.item;
}
//获取最后一个元素
public E getLast() {
final Node<E> l = last;//得到尾节点
if (l == null) //如果为空,抛出异常
throw new NoSuchElementException();
return l.item;
}
//删除第一个元素并返回删除的元素
public E removeFirst() {
final Node<E> f = first;//得到第一个节点
if (f == null) //如果为空,抛出异常
throw new NoSuchElementException();
return unlinkFirst(f);
}
//删除最后一个元素并返回删除的值
public E removeLast() {
final Node<E> l = last;//得到最后一个节点
if (l == null) //如果为空,抛出异常
throw new NoSuchElementException();
return unlinkLast(l);
}
//添加元素作为第一个元素
public void addFirst(E e) {
linkFirst(e);
}
//添加元素作为最后一个元素
public void addLast(E e) {
linkLast(e);
}
//检查是否包含某个元素,返回bool
public boolean contains(Object o) {
return indexOf(o) != -1;//返回指定元素的索引位置,不存在就返回-1,然后比较返回bool值
}
//返回列表长度
public int size() {
return size;
}
//添加一个元素,默认添加到末尾作为最后一个元素
public boolean add(E e) {
linkLast(e);
return true;
}
//删除指定元素,默认从first节点开始,删除第一次出现的那个元素
public boolean remove(Object o) {
//会根据是否为null分开处理。若值不是null,会用到对象的equals()方法
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
//添加指定集合的元素到列表,默认从最后开始添加
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);//size表示最后一个位置,可以理解为元素的位置分别为1~size
}
//从指定位置(而不是下标!下标即索引从0开始,位置可以看做从1开始,其实也是0)后面添加指定集合的元素到列表中,只要有至少一次添加就会返回true
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index); //检查索引是否正确(0<=index<=size)
Object[] a = c.toArray(); //得到元素数组
int numNew = a.length; //得到元素个数
if (numNew == 0) //若没有元素要添加,直接返回false
return false;
Node<E> pred, succ;
if (index == size) { //如果是在末尾开始添加,当前节点后一个节点初始化为null,前一个节点为尾节点
succ = null; //这里可以看做node(index),不过index=size了(index最大只能是size-1),所以这里的succ只能=null,也方便后面判断
pred = last; //这里看做noede(index-1),当然实现是不能这么写的,看做这样只是为了好理解,所以就是在node(index-1的后面开始添加元素)
} else { //如果不是从末尾开始添加,当前位置的节点为指定位置的节点,前一个节点为要添加的节点的前一个节点
succ = node(index); //添加好元素后(整个新加的)的后一个节点
pred = succ.prev; //这里依然是node(index-1)
}
//遍历数组并添加到列表中
for (Object o : a) {
@SuppressWarnings("unchecked")
E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);//创建一个节点,向前指向上面得到的前节点
if (pred == null)
first = newNode; //若果前节点为null,则新加的节点为首节点
else
pred.next = newNode;//如果存在前节点,前节点会向后指向新加的节点
pred = newNode; //新加的节点成为前一个节点
}
if (succ == null) {
//pred.next = null //加上这句也可以更好的理解
last = pred; //如果是从最后开始添加的,则最后添加的节点成为尾节点
} else {
pred.next = succ; //如果不是从最后开始添加的,则最后添加的节点向后指向之前得到的后续第一个节点
succ.prev = pred; //当前,后续的第一个节点也应改为向前指向最后一个添加的节点
}
size += numNew;
modCount++;
return true;
}
//清空表
public void clear() {
//方便gc回收垃圾
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}
//获取指定索引的节点的值
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
//修改指定索引的值并返回之前的值
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
//指定位置后面(即索引为这个值的元素的前面)添加元素
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element); //如果指定位置为最后,则添加到链表最后
else //如果指定位置不是最后,则添加到指定位置前
linkBefore(element, node(index));
}
//删除指定位置的元素,
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
//检查索引是否超出范围,因为元素索引是0~size-1的,所以index必须满足0<=index<size
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
//检查位置是否超出范围,index必须在index~size之间(含),如果超出,返回false
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
//异常详情
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
//检查元素索引是否超出范围,若已超出,就抛出异常
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//检查位置是否超出范围,若已超出,就抛出异常
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//获取指定位置的节点
Node<E> node(int index) {
//如果位置索引小于列表长度的一半(或一半减一),从前面开始遍历;否则,从后面开始遍历
if (index < (size >> 1)) {
Node<E> x = first;//index==0时不会循环,直接返回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;
}
}
//获取指定元素从first开始的索引位置,不存在就返回-1
//不能按条件双向找了,所以通常根据索引获得元素的速度比通过元素获得索引的速度快
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
//获取指定元素从first开始最后出现的索引,不存在就返回-1
//但实际查找是从last开始的
public int lastIndexOf(Object o) {
int index = size;
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (x.item == null)
return index;
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (o.equals(x.item))
return index;
}
}
return -1;
}
//提供普通队列和双向队列的功能,当然,也可以实现栈,FIFO,FILO
//出队(从前端),获得第一个元素,不存在会返回null,不会删除元素(节点)
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//出队(从前端),不删除元素,若为null会抛出异常而不是返回null
public E element() {
return getFirst();
}
//出队(从前端),如果不存在会返回null,存在的话会返回值并移除这个元素(节点)
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//出队(从前端),如果不存在会抛出异常而不是返回null,存在的话会返回值并移除这个元素(节点)
public E remove() {
return removeFirst();
}
//入队(从后端),始终返回true
public boolean offer(E e) {
return add(e);
}
//入队(从前端),始终返回true
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
//入队(从后端),始终返回true
public boolean offerLast(E e) {
addLast(e);//linkLast(e)
return true;
}
//出队(从前端),获得第一个元素,不存在会返回null,不会删除元素(节点)
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//出队(从后端),获得最后一个元素,不存在会返回null,不会删除元素(节点)
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
//出队(从前端),获得第一个元素,不存在会返回null,会删除元素(节点)
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//出队(从后端),获得最后一个元素,不存在会返回null,会删除元素(节点)
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
//入栈,从前面添加
public void push(E e) {
addFirst(e);
}
//出栈,返回栈顶元素,从前面移除(会删除)
public E pop() {
return removeFirst();
}
2.3.Vector实现类。
它是List接口的一个很古老的实现类,线程安全,效率低,底层是数组 Object[] elementData。
Vector:在调用无参构造方法的时候Vector()的时候,底层创建了一个长度为10的数组,如果要扩容,就扩容为两倍。
从JDK1.0就可以看出它很古老,基本不用了,只需要了解即可。
public Vector(int initialCapacity, int capacityIncrement) {
super();
//如果初始化容量参数小于0,报异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
//容量大于等于0.把数组赋值给elementData,增量参数赋值给capacityIncrement
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
public Vector(int initialCapacity) {
//数组初始化容量为10,增量为0
//调用上面的方法
this(initialCapacity, 0);
}
//Vector的无参构造方法,把数组初始化容量设置为10
public Vector() {
//调用上面的构造方法
this(10);
}
面试题:三个主要实现类的区别和联系,主要是区别ArrayList和LinkedList。
相同点:
- 都是list集合的实现类 。
- 存储数据的特点是相同的:有序的、可重复的。
不同点:上述。
3.Set接口
一、Set接口继承Collection接口。
Set接口特点:无序的、不可重复的。
注意:Set没有自己定义的方法,用的都是Collection接口的方法。
它有三个主要实现类:HashSet、LinkedHashSet、TreeSet。
遍历Set的方式(两种)。
Set set = new HashSet();
set.add("aaa");
set.add(123);
set.add("abc");
set.add(12.9);
set.add("aaa");//重复
set.add(new Person("张三",18));
set.add(new Person("张三",18));
//增强for循环
for(Object o : set){
System.out.println(o);
}
//迭代器
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
二、无序性:无序性不是随机性,存储的数据在底层并非按照数组的索引进行添加,而是根据每个元素的哈希值决定。
不可重复性:保证添加的元素按照equals方法进行判断时,不能返回true。
必须重写equals方法和hashCode方法。
三、添加元素底层原理。
- 当你试图把对象加入HashSet时,HashSet会使用对象的hashCode来判断对象加入的位置。
- 如果此位置没有对象,直接把给对象添加到此位置;如果此位置有对象,则比较两个对象的哈希值是否相等。
- 如果哈希值不相等,则通过链地址法将新对象链接到已有对象后面;如果哈希值相等,则通过equals比较两个对象。
- 如果 equals为true 那么HashSet认为新加入的对象重复了,所以加入失败;如果equals 为false那么HashSet 认为新加入的对象没有重复,新对象可以存入。
这里会有一个问题,比较完哈希值后为什么还要用equals进行比较呢?这是因为如果对象的hashCode值是不同的,那么HashSet会认为对象是不可能相等的;但是如果对象不同,哈希值是有可能相同的!
//举例:对象不同但是哈希值相同。
String s1 = new String("重地");
String s2 = new String("通话");
System.out.println(s1.hashCode());//1179395
System.out.println(s2.hashCode());//1179395
哈希值:是一个十进制的整数,由系统随机生成的,它相当于逻辑上的地址值。
Person person = new Person("张三",12);
Person person1 = new Person("张三",12);
//当我没有重写Person的hashCode方法,打印的值是不一样的
System.out.println(person.hashCode());//1323468230
System.out.println(person1.hashCode());//1645995473
//当我重写了Person的hashCode方法,打印的值是不一样的
System.out.println(person.hashCode());//24022532
System.out.println(person1.hashCode());//24022532
//相同元素的哈希值一定相等,但是哈希值相等时,元素不一定相同
//向上不断寻找父类的hashCode方法,会发现有一个31,首先31是素数,减少了哈希值的重复性
String类的hashcode源码如下:
public int hashCode() {
int h = hash; //按照Effective Java的思路,这里的h可以先赋17的初值
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
数值型的一般都直接返回值。
四、介绍实现类。
-
HashSet:主要实现类,迭代输出的元素顺序和插入的顺序不同,底层是以哈希表实现的, 哈希表存放的是哈希值 ,线程不安全。
相当于调用hashmap的无参构造,在hashmap里面定义了一个node类型的数组,初始长度为16。
Set set = new HashSet();
set.add(“aaa”);
set.add(123);
set.add(“abc”);
set.add(12.9);
set.add(“aaa”);//重复
//我重写了Person的hashCode方法,所以这两个对象相同
set.add(new Person(“张三”,18));
set.add(new Person(“张三”,18));for(Object o : set){ System.out.println(o); }//无论遍历几次,输出的顺序依旧不变。 // //aaa //abc //12.9 //com.hpe.collection.Person@4ee285c6 //123
-
LinkedHashSet:是HashSet的子类,可以按照添加元素的顺序进行输出。LinkedHashSet的遍历效率高于HashSet。
Set set1 = new LinkedHashSet();
set1.add(“aaa”);
set1.add(123);
set1.add(“abc”);
set1.add(12.9);
set1.add(“aaa”);//重复set1.add(new Person("张三",18)); set1.add(new Person("张三",18)); for(Object o : set1){ System.out.println(o); } //aaa //123 //abc //12.9 //com.hpe.collection.Person@573fd745
-
TreeSet:可以按照添加对象的指定属性进行排序,底层结构是二叉树,默认从下到大排序。
//排序要求必须是相同类型的对象
Set set = new TreeSet();
set.add(456);
set.add(123);
set.add(234);
//set.add(“abc”);//报错for(Object o : set){ System.out.println(o); } //排序要求相同类型的对象 Set set1 = new TreeSet(); set1.add("b"); set1.add("c"); set1.add("d"); set1.add("a"); for(Object o : set1){ System.out.println(o); }
面试题:List和Set的区别。
- List存储有序的、可重复的元素;Set存储无序的、不可重复的元素。
- List有自己独有的方法;Set没有自己独有的方法。
4.Map接口
一、Map保存的是具有映射关系的数据,保存的是两组值(key和value)。
1.Map和Collection是平行关系。
2.存放的数据都是键值对(key-value)。
二、Map的结构
Map中的key是无序的、不可重复的,它是使用Set存储的。
Map中的value是无序的、可以重复的,它是使用Collection存储的。
key和value的组合是一个Entry对象。
Entry是无序的、不可重复的,它是使用Set存储的。
Map map = new HashMap();
//添加元素put(key,value)
map.put("aaa", 90);
map.put("bbb", new Date());
//向集合添加相同的key,后面的value会覆盖前面的value
map.put("ccc", 90);
map.put(123, "张三");
map.put("ccc", "李四");//把上边的("ccc",90)覆盖了
System.out.println(map);//{aaa=90, ccc=李四, bbb=Sat Feb 27 16:01:06 CST 2021, 123=张三}
三、Map的实现类
- HashMap:Map的主要实现类,线程不安全,效率高,可以存储null的key和value。
- LinkedHashMap:HashMap的子类,保证在遍历map元素的时候,按照添加的顺序进行输出元素值。
原因:在HashMap这个类的基础上,添加了一对指针,指向上一个元素的引用地址和下一个元素的引用地址,遍历效率比HashMap高。 - TreeMap:基于红黑树实现,可以保证key-vlaue进行排序,有序的输出,(自然排序和自定义排序)。
- Hashtable:古老的是实现类,线程安全,效率低,不能存储null的key和value。
- Properties:用来处理配置文件,例如jdbc。
HashMap 和 Hashtable 的区别👇。
Hashtable 是一个线程安全的 Map 实现,但 HashMap 是线程不安全的。
Hashtable 不允许使用 null 作为 key 和 value,而 HashMap 可以。
Map map = new HashMap();
map.put(null, "张三");
map.put("张三", null);
Map map1 = new Hashtable();
//map1.put(null,"张三");//运行报错
//map1.put("张三",null);//运行报错
System.out.println(map);
System.out.println(map1);
四、Map的常用方法(增删改查)
Map map = new HashMap();
//1.添加元素put(key,value)
map.put("aaa", 123);
map.put(123, "abc");
map.put(new Person("张三", 12), 90);
//2.size():获取元素的个数
System.out.println(map.size());//3
//3.remove(Object key):删除某个元素
map.remove(123);
System.out.println(map.size());//2
//4.clear():清除
//map.clear();
//System.out.println(map.size());//0
//5.get(key):
System.out.println(map.get("aaa"));//123
//6.containsKey(key)
System.out.println(map.containsKey("ncnc"));//false
//isEmpty()
System.out.println(map.isEmpty());//false
和遍历有关的方法👇。
-
Set keySet():返回所有的key,构成一个Set集合。
-
Collection values():返回所有的value,构成一个Collection集合。
-
Set entrySet():返回所有的key-value的组合entry,构成一个Set集合。
Map map = new HashMap(); map.put("1001", "阿轲"); map.put("1002", "韩信"); map.put("1003", "李白"); map.put("1004", "百里玄策"); //Set ketSet():获取所有的key Set set = map.keySet(); for (Object o : set) { System.out.println(o); }//1004 1003 1002 1001 System.out.println("===================="); Collection values = map.values(); Iterator i = values.iterator(); while (i.hasNext()) { System.out.println(i.next()); }//百里玄策 李白 韩信 阿轲 System.out.println("====================="); //key-value:通过一个循环获取key-value //遍历map的第一种方法 Set set1 = map.keySet(); for (Object key : set1) { //根据key获取对应的value值 Object value = map.get(key); System.out.println(key + "-" + value); } System.out.println("====================="); //遍历map的第二种方法 Set set2 = map.entrySet(); //从set集合里面取出每一个entry for (Object obj : set2) { //把父类转成子类(强转) Map.Entry entry = (Map.Entry) obj; //获取key Object key = entry.getKey(); //获取value Object value = entry.getValue(); System.out.println(key + "-" + value); }
HashMap的源码分析👇。
//初始化容量(必须是二的n次幂)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//集合最大容量(必须是二的幂)
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子,默认的0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当链表的长度超过8则会转红黑树(1.8新增)
static final int TREEIFY_THRESHOLD = 8;
//当链表的长度小于6则会从红黑树转回链表
static final int UNTREEIFY_THRESHOLD = 6;
//当Map里面的数量超过这个值时,表中的桶才能进行树形化,否则桶内元素太多时会扩容,而不是树形化。为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;
//table用来初始化
transient Node<K,V>[] table;
//map中的每一个元素
transient Set<Map.Entry<K,V>> entrySet;
//HashMap中存储的数量
transient int size;
//HashMap的修改次数
transient int modCount;
//用来调整大小下一个容量的值计算方式为(容量*负载因子)
int threshold;
//哈希表的加载因子
final float loadFactor;
-
table在JDK1.8中我们了解到HashMap是由数组加链表加红黑树来组成的结构,其中table就是HashMap中的数组。
-
size为HashMap中K-V的实时数量。
-
loadFactor加载因子,是用来衡量 HashMap 满的程度,计算HashMap的实时加载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。capacity 是桶的数量,也就是 table 的长度length。
-
threshold计算公式:capacity * loadFactor。这个值是当前已占用数组长度的最大值。过这个数目就重新resize(扩容),扩容后的 HashMap 容量是之前容量的两倍。
//构造一个空的 HashMap,默认初始容量(16)和默认负载因子(0.75)。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}//构造一个空的 HashMap具有指定的初始容量和默认负载因子(0.75)。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}//构造一个空的 HashMap具有指定的初始容量和负载因子。
public HashMap(int initialCapacity, float loadFactor) {
//首先判断初始化容量是否小于0
if (initialCapacity < 0)
//如果小于0,就抛异常
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
//判断是否大于MAXIMUM_CAPACITY
//MAXIMUM_CAPACITY = 1 << 30 = 1073741824
if (initialCapacity > MAXIMUM_CAPACITY)
//如果大于MAXIMUM_CAPACITY,就重新赋值为MAXIMUM_CAPACITY
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
//如果加载因子小于0,或是非数值,就抛异常
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}//最后调用了tableSizeFor,来看一下方法实现:
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
下面我们分析put方法(重点)。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//我们可以看到put调用的是putVal来进行数据插入,但是要注意到key在这里执行了一下hash方法,来看一下Hash方法是如何实现的。
static final int hash(Object key) {
int h;
//key为null的时候,返回的hash为0
//key不为null,首先计算出key的哈希值,然后于h的无条件右移16的值相异或得到hash
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
五、HashMap的底层实现原理(面试重点)
Map map = new HashMap();对加载因子进行了初始化。
map.put(123, 123); 首次调用put方法的时候,创建了一个长度为16的数组Node[],并且对临界值初始化为16*0.75=12。
首次添加元素put(key1,value1)的时候,调用key1的所在类的hashCode方法,计算key1的哈希值,通过一些算法对哈希值进行计算,得到key1元素的位置,如果此位置为空,key1和value1添加成功;
如果此位置上已经有元素了,如果key1的哈希值和已有元素的哈希值不相同,则添加成功。
如果此位置上已经有元素了,如果key1的哈希值和已有元素的哈希值相同,就调用key1的equals方法和key2进行比较,
如果为false,此key1和value1添加成功(以链条的形式存在);如果为true,用value1替换原来的value2。
JDK 1.8 对 HashMap 进行了比较大的优化,底层实现由之前的 “数组+链表” 改为 “数组+链表+红黑树”。
当数组的某一个索引的位置上的元素以链条形式存在的数据个数大于8(默认是8)并且数组的长度大于64,此位置变成红黑树进行存储数据。
最后补充一下Properties。
首先在项目根目录下新建jdbc.properties(与src是兄弟关系)。
jdbc.properties👇。
username=root
password=123456
//Properties:处理配置文件,key和value都是String类型
Properties properties = new Properties();
FileInputStream fis = null;
//1.加载要读取的配置文件
try {
fis = new FileInputStream("jdbc.properties");
properties.load(fis);
//读取配置文件中的相应属性
String username = properties.getProperty("username");
String password = properties.getProperty("password");
System.out.println(username + " " + password);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.Collections工具类
Collection和Collections的区别:
Collections 是一个操作 Set、List 和 Map 等集合的工具类。
Collection是Set和List的父接口。
Collections 中提供了一系列静态的方法对集合元素进行排序(均为static方法)、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。
下面介绍几个常用的方法👇。
List list = new ArrayList();
list.add(123);
list.add(123);
list.add(23);
list.add(45);
list.add(134);
list.add(234);
System.out.println(list);
Collections.reverse(list);//反转排序
System.out.println(list);
Collections.shuffle(list);//随机排序
System.out.println(list);//每次输出都不相同
Collections.sort(list);
System.out.println(list);//从小到大
Collections.swap(list, 1, 2);//交换1和2的元素
System.out.println(list);
Object max = Collections.max(list);//最大值
System.out.println(max);//234
int count = Collections.frequency(list,123);//返回指定元素的出现次数
System.out.println(count);//2
6.泛型
一、泛型的使用:
- jdk5.0出现的一个新特性。
- 可以有效地避免强制类型转换引发的异常。
- 限制集合的数据类型,提高了数据类型的安全性。
- 将运行时异常转成编译成异常。
如果没有泛型,可能会引发类型转换异常👇。
//期末考试成绩出来了,把考试成绩保存到list集合里面,
//取出每一位学生的成绩
List list = new ArrayList();
list.add(90);
list.add(80);
list.add(60);
//没有规定集合只能添加什么类型的数据,默认是Object类型,所以所有类型的数据都可以添加
list.add("abc");
for (int i = 0; i < list.size(); i++) {
//取出每位学生的成绩
//"abc"在强制转换成int时,运行会出现异常
int score = (int) list.get(i);
System.out.println("成绩为 : " + score);
}
使用泛型的集合👇。
//使用泛型的集合
//泛型只能指定是引用数据类型,因为集合保存的都是对象
List<Integer> list = new ArrayList<Integer>();//这个集合只能保存Integer类型的数据
list.add(90);
list.add(80);
list.add(60);
//list.add("abc");//编译时报错
for (int i = 0; i < list.size(); i++) {
int score = (int) list.get(i);
System.out.println("成绩为 : " + score);
}
二、在集合中使用泛型
-
集合接口和集合实现类在jdk5.0中都改成了泛型结构。
-
在实例化集合时,我们可以指定具体的泛型类型。
-
如果在实例化集合时,没有指定泛型类型,默认类型是Object类型。
-
指定完成后,在集合接口或者集合类里,内部结构(属性,方法)使用此泛型的位置,都指定为实例化的泛型类型。
-
注意:泛型的类型必须是类,不能是基本数据类型,如果使用基本数据类型,就转成使用对应的包装类。
常用的几个字母
?:表示不确定java类型。
T(type):表示一个java类型
K(key):map中的key
V(value):map中的value
E(element):代表一个element
举例map泛型。
Map<String, String> map = new HashMap<String, String>();
map.put("1001", "鲁班");
map.put("1002", "孙尚香");
map.put("1003", "百里守约");
//遍历map
Set<Map.Entry<String, String>> set = map.entrySet();
for (Map.Entry<String, String> entry : set) {
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key + "-" + value);
}
三、什么时候使用泛型
- 当操作引用类型不确定的时候可以使用泛型。
- <>是一个用于接收具体类型的代表。
四、泛型类(不常用)
-
定义泛型类:在类名后面通过<>指定一个或者多个类型参数,如果是多个的话,通过逗号隔开class<T, K>。
-
如果在实例化对象时不指定泛型,默认是Object类型。
-
如果在实例化对象时指定了泛型类型,则类后面使用的泛型位置就是实例化所指定的。
package com.hpe.generic;
import java.util.List;
public class Person {
private T t;//根据泛型声明的成员变量 public T getT() { return t; } public void setT(T t) { this.t = t; } public void eat() { System.out.println("人在吃饭"); }
}
//创建一个泛型类
Person p = new Person();
//设置成员变量的值
p.setT(“TalentO_o”);
System.out.println(p.getT());
p.eat();//不适用泛型
Person p1 = new Person();
p1.setT(111);
System.out.println(p.getT());
p.eat();
泛型继承。
/**
* 泛型继承
* 1.全保留
* 2.部分保留
* 3.具体类型
* 4.没有类型
*/
/*父类一般定义为抽象类*/
public abstract class Father<T1,T2> {
T2 name;
public abstract void test(T1 age);
}
//全保留
//子类可以继续添加泛型类型class c1<T1,T2,A,B> 富二代
public class c1<T1,T2> extends Father<T1,T2>{
@Override
public void test(T1 age) {
//this.name = 0; 全保留,不能这样用
}
}
//部分保留
public class c2<T1> extends Father<T1,String>{
@Override
public void test(T1 age) {
this.name = "222"; //引用父类属性,类型随父类而定
}
}
//具体类型
//把T1、T2替换成具体的类型
public class c3 extends Father<Integer, String> {
@Override
public void test(Integer age) {
this.name = "aaa";
}
}
//没有类型 擦除 -->Object
public class c4<A, B> extends Father {
//子类重写方法的参数类型 -->随父类而定:Object
@Override
public void test(Object age) {
}
}
Father<Integer,String> c1 = new c1<Integer,String>();
Father<Integer,String> c2 = new c2<Integer>();
Father<Integer,String> c3 = new c3();
Father c4 = new c4();
五、泛型方法
- 在定义泛型方法时,需要在方法的访问修饰符后面加来指定泛型方法的类型。
- 泛型方法的定义和和所在类是不是泛型类没有关系。
- 使用泛型方法好处:提高了代码的复用性,减少方法重载。
我们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样在初学者中非常容易将泛型方法理解错了。 泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
举例基本用法。
package com.hpe.generic;
import java.util.List;
public class Person<T, K> {
private T t;//根据泛型声明的成员变量
private K k;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
public void eat() {
System.out.println("人在吃饭");
}
//实现数组的复制:泛型方法所在的类可以不是泛型类
//把数组里的元素复制到集合里面
public <E> List<E> copyArrayToList(E[] e, List<E> list) {//它默认会把E当成一个类
//复制
for (E e2 : e) {
list.add(e2);
}
return list;
}
//上面一个方法代替了下面三个重载方法
//public List<String> copyArrayToList(String[] e, List<String> list) {}
//public List<Integer> copyArrayToList(Integer[] e, List<Integer> list) {}
//public List<Double> copyArrayToList(Double[] e, List<Double> list) {}
}
Integer[] integers = new Integer[]{1, 2, 4};
List<Integer> list = new ArrayList<Integer>();
List<Integer> list1 = p.copyArrayToList(integers, list);
System.out.println(list1);
用法解释。
class GenerateTest<T>{
public void show_1(T t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
//由于泛型方法在声明的时候会声明泛型<E>,即使在泛型类中未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
public <E> void show_3(E t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
public <T> void show_2(T t){
System.out.println(t.toString());
}
}
再看一个泛型方法和可变参数的例子:
public class GenerateTest {
public <T> void printMsg(T... args) {
for (T t : args) {
System.out.println("t is " + t);
}
}
}
GenerateTest generateTest = new GenerateTest();
generateTest.printMsg("111", 222, "aaaa", "2323.4", 55.55);
泛型通配符上下界限
- <? extends T>:是指上界通配符。
- <? super T>:是指 下界通配符。
为什么要用通配符和边界?
使用泛型的过程中,经常出现一种很别扭的情况。比如按照题主的例子,我们有Fruit类,和它的派生类Apple类。
class Fruit {}
class Apple extends Fruit {}
然后有一个最简单的容器:Plate类。盘子里可以放一个泛型的“东西”。我们可以对这个东西做最简单的“放”和“取”的动作:set( )和get( )方法。
class Plate<T>{
private T item;
public Plate(T t){item=t;}
public void set(T t){item=t;}
public T get(){return item;}
}
现在我定义一个“水果盘子”,逻辑上水果盘子当然可以装苹果。
Plate<Fruit> p=new Plate<Apple>(new Apple());
但实际上Java编译器不允许这个操作。会报错,“装苹果的盘子”无法转换成“装水果的盘子”。
error: incompatible types: Plate<Apple> cannot be converted to Plate<Fruit>
就算容器里装的东西之间有继承关系,但容器之间是没有继承关系的。所以我们不可以把Plate的引用传递给Plate。
什么是上界?
下面代码就是“上界通配符(Upper Bounds Wildcards)”:
Plate<? extends Fruit>
Plate<? extends Fruit>和Plate最大的区别就是:Plate<? extends Fruit>是Plate以及Plate的基类。直接的好处就是,我们可以用“苹果盘子”给“水果盘子”赋值了。
Plate<? extends Fruit> p=new Plate<Apple>(new Apple());
如果把Fruit和Apple的例子再扩展一下,食物分成水果和肉类,水果有苹果和香蕉,肉类有猪肉和牛肉,苹果还有两种青苹果和红苹果。
//Lev 1
class Food{}
//Lev 2
class Fruit extends Food{}
class Meat extends Food{}
//Lev 3
class Apple extends Fruit{}
class Banana extends Fruit{}
class Pork extends Meat{}
class Beef extends Meat{}
//Lev 4
class RedApple extends Apple{}
class GreenApple extends Apple{}
什么是下界?
相对应的,“下界通配符(Lower Bounds Wildcards)”:
Plate<? super Fruit>
表达的就是相反的概念:一个能放水果以及一切是水果基类的盘子。Plate<? super Fruit>是Plate的基类,但不是Plate的基类。对应刚才那个例子,Plate<? super Fruit>覆盖下图中红色的区域。
上界<? extends T>不能往里存,只能往外取。
下界<? super T>不影响往里存,但往外取只能放在Object对象里。
六、泛型接口(经常用)
三层架构中有一层叫做dao层—数据库访问(访问)层,操作数据库,对数据库数据进行增删改查。
Dao接口👇。
package com.hpe.generic;
import java.util.List;
//泛型接口 dao对数据库表进行crud操作
//一个项目有n个模块,每一个模块都有crud操作
//数据库中的表映射到java项目中的实体类
//对User、News进行crud操作
public interface Dao<T> {
//添加
void add(T t);
//修改
void update(T t);
//删除
void delete(int id);
//查询
List<T> getList();
}
User实体类👇。
package com.hpe.generic;
public class User {
private String name;
private String pwd;
public User() {
}
public User(String name, String pwd) {
this.name = name;
this.pwd = pwd;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", pwd='" + pwd + '\'' +
'}';
}
}
UserDaoImpl实现类👇。
package com.hpe.generic;
import java.util.ArrayList;
import java.util.List;
public class UserDaoImpl implements Dao<User>{
//ctrl+i自动重写方法后如下
@Override
public void add(User user) {
}
@Override
public void update(User user) {
}
@Override
public void delete(int id) {
}
@Override
public List<User> getList() {
//模拟从数据库得到数据
List<User> list = new ArrayList<User>();
list.add(new User("admin","admin"));
list.add(new User("张三","zhangsan"));
list.add(new User("李四","lisi"));
return list;
}
}
//泛型接口的使用
UserDaoImpl UserDaoImpl = new UserDaoImpl();
List<User> list2 = UserDaoImpl.getList();
for (User user1 : list2) {
System.out.println(user1);
}
七、通配符的使用
泛型在继承方面:虽然A是B的父类,但是E和E两者没有任何关系。
但是我们可以找一个通用的父类。
Object obj = null;
String str = null;
obj = str;//多态,因为obj是父类,str是子类
Object[] arr1 = null;s
String[] arr2 = null;
arr1 = arr2;//可以,因为是父子关系
//此时list1和list2类型没有任何关系,没有父子关系
//泛型是没有继承关系的
List<Object> list1 = null;
List<String> list2 = new ArrayList<String>();
//list1 = list2;//报错
List<?> list = null;//它就变成了一个通用的父类
list = list1;
list = list2;
7.异常
- 任何一种程序设计语言设计的程序在运行时都有可能出现错误,例如除数为0,数组下标越界,要读写的文件不存在等。
- 捕获错误最理想的是在编译期间,但有的错误只有在运行时才会发生。
- 对于在运行时发生的错误,一般有两种解决方法:(哪一个更好)
- 遇到错误就终止程序的运行。
- 由程序员在编写程序时,就考虑到错误的检测、错误消息的提示,以及错误的处理。
- 异常:程序执行中发生的不正常情况。
- 开发过程中的语法错误不叫异常
- Java的异常处理:处理非预期的情况,如文件没找到,空指针等。
- Java程序运行过程中所发生的异常事件可分为两类:
- Error: JVM系统内部错误、资源耗尽等严重情况//无法控制,不处理。
- Exception: 其它因编程错误或偶然的外在因素导致的一般性问题。如:空指针访问、试图读取不存在的文件等。
先来举个例子👇。
Scanner sc = new Scanner(System.in);
System.out.print("请输入年龄(年龄为整数):");
int age = sc.nextInt();
//输入a
//运行时报异常
System.out.println("年龄为" + age);
System.out.println("aaaa");//不会被执行
一、什么是异常?
在一个在程序执行期间发生的事件,它中断正在执行程序的正常指令流。
程序运行—出现异常—程序中断。
异常出现的情况:
- 内存耗尽了,无法实例化。
- JVM崩溃。
- 数组下标越界异常。
- 算术异常…
二、异常体系
- 所有异常类的祖先是Throwable
- Throwable有两个子类:Error和Exception
- Error:是程序无法处理的错误,表示运行应用程序中较严重问题。
public static void main(String[] args) {
//通过编译,运行时报异常
//Exception in thread “main” java.lang.StackOverflowError:栈溢出
main(args);
} - Exception:在程序开发期间要处理这种异常。
-
IOException:编译时异常。
//编译时异常
//Unhandled exception: java.io.FileNotFoundException
//因为找不到aa.txt文件,所以报异常,不能通过编译
FileInputStream fis = new FileInputStream(new File(“aa.txt”));
int b;
while ((b = fis.read()) != -1) {
System.out.println((char) b);
}
fis.close(); -
RunTimeException:运行时异常。
-
- Error:是程序无法处理的错误,表示运行应用程序中较严重问题。
三、常见的几种运行时异常。
-
数组下标越界异常:ArrayIndexOutOfBoundsException。
int[] num = new int[5];
//运行时报异常:java.lang.ArrayIndexOutOfBoundsException: 5
//数组下标越界异常
num[5] = 20;
System.out.println(num[5]); -
算术异常:java.lang.ArithmeticException。
int i = 6;
//java.lang.ArithmeticException: / by zero
System.out.println(i / 0); -
空指针异常:java.lang.NullPointerException。
String[] strs = null;
//java.lang.NullPointerException
System.out.println(strs[0]); -
类型转换异常:java.lang.ClassCastException。
Object date = new Date();
//java.lang.ClassCastException
String str = (String) date;
四、如何处理Exception异常
java处理异常的方式:抓抛模型。
- 抛:当执行程序的时候,一旦出现异常,就会生成一个对应的异常对象,并将其抛出。
(1)抛的类型:自动抛(常用)和手动抛。
(2)此异常对象处理交给方法的调用者。
一旦出现异常,就会生成一个对应的异常对象,并将其抛出,将来交给方法的调用者。 - 抓:抓住程序中抛出来的异常对象。
在java程序运行时收到异常对象,会寻找能够处理这种异常的异常代码,并将当前的异常对象交给它处理。
怎么抓异常对象?抓的过程就是异常处理的方式。
java处理异常提供了两种方式。
-
try-catch
try {
} catch (异常对象1 e) {
//处理异常的方式
} catch (异常对象2 e) {
//处理异常的方式
} catch (异常对象3 e) {
//处理异常的方式
}finally{
//一定会被执行的代码
}
(1)try里面放的是可能会出现异常的代码,同时要清除在try中定义的变量,它里面定义的变量是局部变量,作用在try代码块中。
(2)catch用来处理异常-
System.out.println(“出现了异常”);自定义打印信息
-
e.printStackTrace();//打印堆和栈的错误信息(程序出错的位置及原因)
-
e.getMessage();//打印错误信息
(3)可以定义多个catch,try中抛出的异常对象从上往下进行匹配异常类型,只要匹配成功,就执行完此catch里的代码,然后跳出,执行下面的代码。
(4)如果有多个catch,多个catch的异常对象是并列关系,就不需要考虑定义的顺序。
(5)如果多个异常对象有继承关系,就需要从小到大定义,即先指定子类再指定父类。
注意:如果异常处理了,那么后面的代码会继续执行。
(6)finally是可选的代码块,如果加了finally,无论是否出现异常,都会执行finally里的代码。
下面处理运行时异常。
try {
int[] num = null;
num = new int[5];
num[5] = 20;
System.out.println(num[5]);
} catch (ArrayIndexOutOfBoundsException e) {
//System.out.println(“数组下标越界了…”);
//e.printStackTrace();//打印堆和栈的错误信息
System.out.println(e.getMessage());;//打印错误信息
}
try {
int i = 6;
System.out.println(i / 0);
}catch (ArithmeticException e){
e.printStackTrace();
}System.out.println(“aaa”);//会被执行
try{
String[] strs = null;
//java.lang.NullPointerException
System.out.println(strs[0]);
}catch (NullPointerException e){
e.printStackTrace();
}
try {
Object date = new Date();
String str = (String) date;
}catch (ClassCastException e){
e.printStackTrace();
}catch(NullPointerException e){
e.printStackTrace();
}catch (ArithmeticException e){
e.printStackTrace();
}catch(RuntimeException e){
e.printStackTrace();
}catch (Exception e){
e.printStackTrace();
}
//关于异常对象的定义顺序其实很好理解,假如可以先指定父类再指定子类,如果父类的匹配成功,那么直接跳出,不会匹配子类,如果父类没有匹配成功,那么作为其子类,自然也不会匹配成功。
运行时异常可以不显示的进行异常处理。
编译时异常必须显式的异常处理,否则不会通过编译。
处理运行时异常和编译时异常方式是一样的,区别就是编译时异常必须要进行处理,否则不能通过编译。
FileInputStream fis = null;
try {
fis = new FileInputStream(new File(“aa.txt”));
int b;
while ((b = fis.read()) != -1) {
System.out.println((char) b);
}
System.out.println(“aaa”);//不会被执行
} catch (Exception e) {
e.printStackTrace();
} finally {
//无论有没有异常都会执行
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println(“bbb”);//会被执行
finally很强大,即使前面遇到return,也会执行。
package com.hpe.java;public class TestFinally {
public static void main(String[] args) { //test(); int i = test1(); System.out.println(i);//2 } public static void test() { try { int i = 6; System.out.println(i / 0);//遇到异常 System.out.println("aaa");//不会被执行 } catch (Exception e) { e.printStackTrace(); int i = 6; System.out.println(i / 0);//遇到异常 System.out.println("aaa");//不会执行 }finally { System.out.println("TalentO_o");//会被执行 } } public static int test1() { try { return 1; //即使这里return了,下面的finally也会被执行。 } catch (Exception e) { e.printStackTrace(); } finally { System.out.println("TalentO_o"); return 2; } }
}
-
-
throws:找别人帮我处理异常。
(1)在方法的声明处,显示的抛出异常对象类型(委托处理)。
(2)语法:
public static void test1() throws FileNotFoundException, IOException{}
(3)处理方式:
-
该方法出现异常时,会抛出一个异常对象,抛给方法的调用者。
-
异常对象可以一直向上抛,直到抛给main方法,如果main不想处理,就交给jvm。
package com.hpe.java;import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;public class TestException2 {
public static void main(String[] args){ //main方法处理异常 //当然main方法也可以不处理异常,方法后面也可以加throws Exception,然后交给jvm处理,但是一般抛给main方法之后就不再网上抛了 try { test3(); } catch (Exception e) { e.printStackTrace(); } } public static void test3() throws Exception{ //test2不想处理异常,因为main方法调用了test3,所以把异常抛给main处理 test2(); } public static void test2() throws Exception{ //test2不想处理异常,因为test3方法调用了test2,所以把异常抛给test3处理 test1(); } public static void test1() throws FileNotFoundException, IOException{ //执行时遇到异常,因为test2方法调用了test1,所以把异常抛给test2处理 FileInputStream fis = new FileInputStream(new File("aa.txt")); int b; while ((b = fis.read()) != -1) { System.out.println((char) b); } fis.close(); }
}
-
总结:
- 抓:异常处理。
两种处理方式:try-catch和throws+异常类型。 - 抛:在程序执行期间,一旦出新异常,会抛出异常类对象。
两种方式:自动抛和手动抛(throw+异常类的对象),异常类可以自己定义。
下面解释一下手动抛(throw)。
package com.hpe.java;
//比较两个圆对象的半径大小
//手动抛出异常
//抛出的时异常类型,如果是RuntimeException,可以直接抛,不用try-catch
//如果是Exception,就必须加try-catch,因为它包含编译时异常,我们抛的是运行时异常
public class Circle {
private double r;
public double getR() {
return r;
}
public void setR(double r) {
this.r = r;
}
public Circle() {
}
public Circle(double r) {
this.r = r;
}
public int compareTo(Object o){
if(this == o){
return 0;
}else if (o instanceof Circle){
Circle c = (Circle) o;
if(this.getR() > c.getR()){
return 1;
}else if(this.getR() < c.getR()){
return -1;
}else{
return 0;
}
}else{
//类型不匹配,手动抛出异常
//throw new RuntimeException("输入的类型不匹配");
//try {
// //Exception包括编译时异常和运行时异常,但是肯定是不能抛编译时异常的
// //用throw抛异常的时候出现了异常,所以用try-catch
// throw new Exception("输入的类型不匹配");
//} catch (Exception e) {
// e.printStackTrace();
//}
//自定义MyException异常类
throw new MyException("输入的类型不匹配");
}
}
}
package com.hpe.java;
public class TestCircle {
public static void main(String[] args) {
Circle c = new Circle(1);
Circle c1 = new Circle(2);
String str = new String("3");
System.out.println(c.compareTo(str));;
}
}
package com.hpe.java;
/**
* 如何自定义一个异常类?
* 1.自定义的异常类需要显示的继承RuntimeException
* 2.提供序列号
* 3.提供一个无参和有参的构造方法
*/
public class MyException extends RuntimeException {
static final long serialVersionUID = -7034897190745766939L;
public MyException() {
}
public MyException(String message) {
super(message);
}
}