一、异常
1、异常体系结构
java. lang.Throwable
l-----java. lang. Error:一般不编写针对性的代码进行处理。
l-----java.lang.Exception:可以进行异常的处理
l-----编译时异常(checked)
l -----IOException
l -----FileNotFoundException
l-----ClassNotFoundException
l-----运行时异常(unchecked)
l-----NullPointerException
l-----ArrayIndexoutOfBoundsException
l-----ClassCastException
l-----NumberFormatException
l-----InputMismatchException
l-----ArithmeticException
1、异常的处理
1.异常的处理:抓抛模型
过程一:"抛":程序在正常执行的过程中,一旦出现异常,就会在异常代码处生成一个对应异常类的对象。并将此对象抛出。一旦抛出对象以后,其后的代码就不再执行。
过程二:"抓":可以理解为异常的处理方式: ① try-catch-finally ② throws
2.try-catch-finally的使用
try{
//可能出现异常的代码
}catch(异常类型1 变量名1){
//处理异常的方式1
}catch(异常类型2变量名2){
//处理异常的方式2
}catch(异常类型3变量名3){
//处理异常的方式3
}
finally{
//一定会执行的代码
}
说明:
1、finally是可选的。
2、使用try将可能出现的异常包起来,在执行过程中,一旦出现异常,就会生成对应的异常类的对象,根据此类对象的类型,去catch中进行匹配。
3、一旦try中的异常对象匹配到某个catch时,就进入catch中处理异常。一旦处理完成,就跳出当前的try-catch结构(在没有finally的情况),继续执行下面的代码。
4、catch 中的异常类型如果没有满足子父类关系,则谁声明在上,声明在下不重要。
catch 中的异常类型如果满足子父类关系,则子类要声明在父类上面。否则,报错。
5、常用的异常对象处理的方式:①String getMessage() ②printStackTrace().
6、在try结构中声明的变量,再出了try结构以后,就不能再被调用。
体会1:使用try-catch-finally处理编译时异常,是得程序在编译时就不再报错,但是运行时仍可能报错。相当于我们使用try-catch-finally将一个编译时可能出现的异常,延迟到运行时出现。
体会2:开发中,由于运行时异常比较常见,所以我们通常不对运行时异常进行try-catch-finally处理。针对编译时异常,都要进行处理。
3.throws + 异常类型
1."throws + 异常类型"写在方法的声明处。指明此方法执行时,可能会抛出的异常类型。
一旦当方法体执行时,出现异常,仍会在异常代码处生成一个异常类的对象,此对象满足throws后异常类型时,就会被抛出。异常代码后续的代码,就不再执行!
2.体会: try-catch-finally:真正的将异常给处理掉了。
throws的方式只是将异常抛给了方法的调用者。并没有真正将异常处理掉。
3、开发中如何选择使用try-catch-finally 还是使用throws?
1)如果父类中被重写的方法没有throws方式处理异常,则子类重写的方法也不能使用throws,意味着如果子类重写的方法中有异常,必须使用try-catch-finally方式处理。
2) 执行的方法A中,先后又调用了另外的几个方法,这几个方法是递进关系执行的。我们建议这几个方法使用throw的方式进行处理。而执行的方法A可以考虑使用try-catch-finally方式进行处理。
二、多线程
1、程序、进程、线程
程序:是一段静态的代码,静态对象。
进程:正在运行的程序。
1)进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
线程:是一个程序内部执行的一条路径。
1)线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
2)一个进程中的多个线程共享相同的内存单元/内存地址空间→它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
进程可以细分为多个线程:
每个线程,拥有自己独立的:栈、程序计数器。
多个线程,共享同一个进程中的结构:方法区、堆。
并行并发:
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
2、多线程的创建
1.方式一:Thread类
创建步骤
1、创建一个继承于Thread类的子类
2、重写Thread类的run()方法--->将此线程执行的操作声明在run()中
3、创建Thread类的子类的对象
4、通过此对象调用start() ①启动当前线程 ②执行run()方法
创建多个线程,就需要创建多个对象。
MThread m1=new MThread();
MThread m2=new MThread();
MThread m3=new MThread();
m1.start();
m2.start();
m3.start();
测试Thread中的常用方法:
1. start():启动当前线程;调用当前线程的run()
2. run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3. currentThread():静态方法,返回执行当前代码的线程
4. getName():获取当前线程的名字
5. setName():设置当前线程的名字
6. yield()∶释放当前cpu的执行权
7.join():在线程α中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
9. sleep(Long millitime):让当前线程*睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
10. isAlive():判断当前线程是否存活。
线程的优先级
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5 -------默认优先级
如何获取和攻置当前线程的优先级:
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级
说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
2.方式二:实现runnable接口
1、创建一个实现了runnable接口的类
2、实现类去实现runnable中的抽象方法:run();
3、创建实现类的对象
4、将此对象最为参数传递到Thread类的构造器中,创建Thread类的对象
5、通过Thread类的对象调用start();
MRunnable mr=new MRunnable;
Thread m1=new Thread(mr);
Thread m2=new Thread(mr);
Thread m3=new Thread(mr);
m1.start();
m2.start();
m3.start();
比较runnable接口和Thread类
开发中:优先选择:实现runnable接口的方式。
原因: 1、实现的方式没有类的单继承的局限性。
2、实现的方式更适合来处理多个线程共享数据的情况。
联系:在Thread类中其实也实现了runnable接口。
相同点:两种方式都要重写run(),将线程要执行的逻辑声明在run()中。
3.方式三:实现Callable接口
//1.创建一个实现Callable接口的实现类
class MCall implements Callable{
//2.实现call方法,将此线程需要声明的操作声明在call()中
@Override
public Object call() throws Exception{ return 100;}
}
public class Test{
main(){
//3.创建Callable接口实现类的对象
MCall call=new MCall();
//4.将Callable接口实现类的对象传递到FutureTask构造器中,创建FutureTask对象
FutureTask futureTask=new FutureTask(call);
//5.将FutureTask对象传递到Tread类的构造器中,创建Thread对象,并调用start()
//首先FutureTask底层也是实现了runnalbe接口 ,所以才可以使用
new Thread.start(futureTask);
//6.可选 获取call方法中的返回值 因为有异常需要使用try catch就不写了
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
Object num=futureTask.get();
}
}
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
1.call()可以有返回值的。
2.call()可以抛出异常,被外面的操作捕获,获取异常的信息
3. Callable是支持泛型的
4.方式四:线程池
class MRunnable implements Runnable{
public void run(){
//内容
}
}
public class Test{
main(){
//1.提供指定线程数量的线程池 因为接口 内容很少
ExecutorService service=Executors.newFixedThreadPool(10);
//需要使用线程池的属性时,需要使用ExecutorService接口的实现类的子类ThreadPoolExecutor
ThreadPoolExecutor service1=(ThreadPoolExecutor) service;
service1.setCorePoolSize(10);//核心池的大小
service1.setMaximumPoolSize(100);//最大线程数
//2.执行指定的线程操作。需要提供Runnable或者是Callable接口
service.execute(new MRunnable); //用于Runnable接口
//service.submit(); //适用于Callable接口
//关闭线程池
service.shutdown();
}
}
线程池的好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理
corePoolsize:核心池的大小
maximumPooLsize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
线程的分类
java中线程的分类分为两类: ①守护线程 ②用户线程
用户线程死亡时,守护线程也一起死亡。
使用方式:通过在start()方法前调用thread.setFaemon(true)可以把一个用户线程转为守护线程。
3、线程的生命周期
4、线程的安全问题
线程如果共享数据那就变得不安全,所以引入同步。
方式一、同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:1、操作共享数据的代码,即为需要被同步的代码。
2、共享数据:多个线程共同操作的变量。
3、同步监视器:俗称 :锁,任何一个对象都可以充当锁。
要求:多个线程必须用同一把锁。
补充:在使用runnable接口创建同步监视器中,我们可以考虑使用this当同步监视器。
在使用Thread类创建同步监视器中,需要慎用this当同步监视器,可以使用反射的类当同步监视器。
方式二、同步方法
权限类型 synchronized void Xxx(){//Thread需要将方法改为static,runnable不需要更改
//需要被同步的代码
}
关于同步方法的总结:
1.同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
2.非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身
方式三、Lock锁
ReentrantLock类实现了Lock
//1、实例化ReentrantLock
private ReentrantLock lock=new ReentrantLock();
//默认 ReentrantLock()为false,当设置为ReentrantLock(true)
//就相当于在进入同步代码之前就会分配好位置,在执行完全部分配的位置后,才会再一次分配位置
try{
//2、调用锁定方法lock()
lock.lock();
需要被同步的代码
}
finally{
//3、调用解锁方法:unlock()
lock.unlock();
}
1.面试题: synchronized 与Lock的异同?
相同:二者都可以解决线程安全问题
不同: synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
Lock需要手动的启动同步(Lock() ),同时结束同步也需要手动的实现(unlock())
2.优先使用顺序:
Lock 同步代码块(已经进入了方法体,分配了相应资源)→同步方法(在方法体之外)
同步的方式解决的线程的安全问题------好处
操作同步代码时,只有一个线程参与,其他线程等待,相当于是一个单线程过程。-----局限性
死锁的问题
1.死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
2.说明:
1)出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
2)我们使用同步时,要避免出现死锁。
线程通信
涉及到的三个方法:
wait( ):一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
说明:
1.wait( ) ,notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
2.wait( ),notify(), notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器否则,会出现ILlegaLMonitorStateException异常
3.wait(),notify().notifyAll()三个方法是定义在java.lang.0bject类中。
面试题: sLeep()和wait()的异同?
1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:
1)两个方法声明的位置不同:Thread类中声明sleep() , object类中声明wait()。
2)调用的要求不同: sleep()可以在任何需要的场景下调用。wait()必须使用在同步代码块或者同步方法中。
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
三、常用类
1、String类
1.String常用类
1、String类被声明为final,不可被继承。
2、String实现了Serializable接口:表示字符串是支持序列化的。
实现了Comparable接口:表示String可以比较大小
3、String内部类定义了final char[] value用于存储字符串数据
4.String:代表不可变的字符序列。简称:不可变性。
方法传递形参也是不可变。
体现:
1)当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值
2)当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
3)当调用String 的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
4)通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
5)字符串常量池中是不会存储相同内容的字符串的。
2.String实例化方式:
方式一:通过字面量的方式
方式二:通过new+构造器方式
面试题: String s = new String("abc");方式创建对象,在内存中创建了几个对象?
答:两个,一个是堆空间中new结构,另一个是char[]对应的常量池中的数据: "abc"
下面的图要注意常量和常量的拼接结果在常量池,如果声明为final 那么结果也在常量池
3、String常用方法
4、String和char[]与byte[]之间转换
与char[]的转换
String ---> char[]:调用String的方法 toCharArray( )
char[] ---> String:调用String的构造器
与byte[]之间的转换
编码:String --->byte[]:调用String的getBytes()
解码:byte[] --->String:调用String的构造器
编码:字符串-->字节(看得懂--->看不懂的二进制数据)
解码:编码的逆过程,字节-->字符串(看不懂的二进制数据---〉看得懂)
说明解码时,要求解码使用的字符集必须与编码时使用的字符集一致,否则会出现乱码。
2、StringBuffer 和StringBuilder
1.String、StringBuffer、StringBuilder三者的异同?
相同点:
底层使用char[]存储
不同点:
String:不可变的字符序列
StringBuffer:可变的字符序列;线程安全的,效率低;
StringBuilder:可变的字符序列; jdk5.e新增的,线程不安全的,效率高;
//源码分析
String str = new String(); //char[] value = new char[0];
string str1 = new String( "abc" ); //char[] value = new char[]{'a','b','c'};
StringBuffer sb1 = new StringBuffer();//char[] value =new char[16]; //底层创建了一个长度默认为16的数组
sb1.append('a'); //value[0] = 'a';
sb1.append('b'); // value[1] = 'b';
StringBuffer sb2 = new StringBuffer("abc"); //char[] value = new char ["abc".length()+16]
//问题1.
System.out.println(sb1.length());//0
System.out.println(sb2.length());//3
问题2.扩容问题:如果要添加的数据底层数组盛不下了,那就需要扩容底层的数组。默认情况下,扩容为原来容量的2倍+ 2,同时将原有数组中的元素复制到新的数组中。
开发中建议直接指定StringBuffer、StringBuilder的容量,尽量避免发生扩容问题
2.StringBuffer和StringBuilder的常用方法
3、对比String、StringBuffer、StringBuilder三者的效率
从高到低排列:StringBuilder >StringBuffer > String
2、时间类 p467~468
四、枚举类与注解
五、集合
1、集合框架概述
1.集合、数组都是对多个数据进行存储操作的结构,简称ava容器。说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储。
2.1数组在存储多个数据方面的特点:
1、—旦初始化以后,其长度就确定了。
2、数组一旦定义好,其元素的类型也就确定了。我们也就只能操作指定类型的数据了。
比如: String[] arr;int[ ] arr1;object[ ] arr2;
2.2数组在存储多个数据方面的缺点:
1) —旦初始化以后,其长度就不可修改。
2)数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。
3)获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用
4)数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。
2、集合框架
|--- collection接口:单列集合,用于存储一个个的对象
|---List接口:存储有序的、可重复的数据
|---ArrayList(扩容1.5倍)、LinkedList、Vector(扩容2倍)
|---Set接口:存储无序的、不可重复的数据
|---HashSet、LinkedHashSet、TreeSet
|---Map接口:多列集合,用于存储一对一对的数据(key--value)
|---HashMap、LinkedHashMap、TreeMap、Hashtable、Properties
3、增强for循环
jdk5.0新增
1、格式:
for(集合元素类型 局部变量 :集合对象){ }
2、增强for循环内部使用的也是iterator
3、只能使用在数组和集合中
4、增强for循环修改内容不会改变原先的集合或者数组
4、collection接口
1.collection的方法
集合的遍历操作可以使用迭代器iterator接口
1、内部方法hasNext() 和 next()
2、集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
3、iterator主要用于遍历collection
2、collection子接口之一 :List接口
List接口的框架:
|--- collection接口:单列集合,用于存储一个个的对象
|---ArrayList:作为List接口的主要实现类;线程不安全的,效率高;底层使用Object[]
|---LinkegList:对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层
|---vector:作为List接口的古老实现类;线程安全的,效率低;底层使用object[]
1. 面试题: ArrayList、 LinkedList、 Vector三者的异同?
同:三个类都是实现了List接口,存储数据的特点相同:存储有序的、可重复的数据
不同:
如上(List接口的框架)
2. ArrayList源码分析:
1)jdk 7的情况下:
ArrayList list = new ArrayList();//底层创建了长度是10的object[]数组eLementData
list.add(123); //eLementData[e] = new Integer(123);
...
list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。
默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。
2)jdk 8的情况下:
ArrayList list = new ArrayList();//底层object[] elementData初始化为{ }.并没有创建长度为10的数组
list.add(123);//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData中
后续的添加和扩容操作与jdk 7无异。
3)小结:
jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。
3.List的方法
//1.iterator迭代器遍历
Iterator iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
//2.foreach循环遍历
for(Object obj:list){
System.out.println(obj);
}
//3.普通for循环
for (int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
3、collection子接口之一 :Set接口
Set接口的框架:
|---collection接口:单列集合,用来存储一个一个的对象
|---SeI接口:存储无序的、不可重复的数据
|---HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值
|---LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的进行查看
|---TreeSet:可以按照添加对象的指定属性,进行排序。
1、Set接口中没有额外定义新的方法,使用的都是Collection中声明过的方法。
2、要求:向Set中添加的数据,其所在的类一定要重写hashcode()和equals()
要求:重写的hashCode()和equals()尽可能保持一致性:相等的对象必须具有相等的散列码
一、Set:存储无序的、不可重复的数据
以HashSet为例:
1、无序性:不等于随机性,存储的数据在底层数组并非按照索引的顺序添加,而是根据HashCode值来添加。
2、不可重复性:保证元素按照equals()来进行判断,相同的元素只能出现一次。
二、添加元素的过程:
以HashSet为例:
我们向HashSet中添加元素a,首先调用元素a所在类的hnashCode()方法,计算元素a的哈希值,此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置),判断数组此位置上是否已经有元素:
如果此位置上没有其他元素,则元素a添加成功。--->情况1
如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
如果hash 值不相同,则元素a添加成功。--->情况2
如果hash值相同,进而需要调用元素a所在类的equlas()方法:
equals()返回true,元素α添加失败
equaLs(()返回false,则元素α添加成功。--->情况3
对于情况2和情况3而言:元素a,与已经存在指定位置上的元素成链表方式存储。
jdk7:元素a,放到数组中,指向原来的元素。
jdk8:数组中的其他元素,指向元素a。
总结:七上8下
HashSet的底层使用数组加链表的方式存储。
三、LinkedHashSet的使用
LinkedHashSet作为HashSet的子类,再添加数据的同时,每个数据还维护了两个引用,记录此数据的前一个数据和后一个数据。
优点:对于频繁的遍历操作,LinkedHashSet的效率要高于HashSet
四、TreeSet的使用
1、向TreeSet中添加的数据,要求是相同类的对象。
2、两种排序方式:自然排序(实现Comparable接口)、定制排序(实现Comparator接口)
3、自然排序中,比较的是两个对象是否相同的标准:compareTo()返回0,不是使用equals()
5、Map接口
一、Map接口的框架
|---Map:双列数据,存储key-value对的数据
|---HashMap:作为Map的主要实现类;线程不安全的,效率高;存储nuLl的key和value
|---LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。
原因:在原有的HashNap底层结构基础上,添加了一对指针,指向前一个和后一个元素。
对于频繁的遍历操作,此类执行效率高于HashMap 。
|---TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序 ,底层使用红黑树
|---Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
|---Properties:常用来处理配置文件。key和vaLue都是String类型
HashMap的底层:数组+链表(jdk7及之前)
数组+链表+红黑树(jdk8)
面试题:
2.HashMap和 Hashtable的异同?
3. CurrentHashMap 与HashtabLe的异同?(暂时不讲)
二、Map结构的理解:
Map中的key:无序的、不可重复的,使用Set存储所有的key ---〉 key所在的类要重写equals()和nashCode()(UHashMap为例)
Map中的value:无序的、可重复的,使用Collection存储所有的value --->value所在的类要重写equals()
一个键值对: key-value构成了一个Entry对象。
Map中的entry:无序的、不可重复的,使用Set存储所有的entry
三、HashMap的底层实现原理
以jdk7为例说明:
HashMap map = new HashMap();
在实例化以后,底层创建了长度为16的一维数组Entry[ ] table。
...可能已经执行过多次put. . .
map.put( key1, value1):
首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。如果此位置上的数据为空,此时的key1-value1添加成功。----情况1
如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2
如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)
如果equals()返回faLse:此时key1-value1添加成功。----情况3
如果equals()返回true:使用value1替换value2。
补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。
在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空时),扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。
jdk8 相较于jdk7在底层实现方面的不同:
1. new HashMap():底层没有创建一个长度为16的数组
2.jdk 8底层的数组是:Node[].而非Entry[ ]
3.首次调用put()方法时,底层创建长度为16的数组
4. jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。
当数组的某一个索引位置上的元素以链表形式存在的数据个数>8且当前数组的长度>64时,此时此索引位置上的所有数据改为使用红黑树存储。
四、Map的常用方法
6、Collections工具类
Collections:操作Collection、Map的工具类