1.异常
概述
Throwable()
是Java中所有错误和异常的超类。
Error()
是严重问题,Java无法进行处理,比如硬件问题、内存资源不足等。
出现异常时JVM的默认处理方案
异常处理
为什么要自己进行异常处理?因为JVM的默认处理方案会让程序在出现问题的地方停止运行,而在实际开发中程序某一部分出现问题不应影响后续的执行。
try…catch…
Throwable类的成员方法
public String getMessage()
:返回出现异常的原因。
public String toString()
:返回出现异常的原因及异常类名。(包含了getMessage()
的内容)
public void printStackTrace()
:返回出现异常的原因、类名以及异常出现位置。因为输出的信息是最全面的,所以该方法是最常用的。
编译时异常和运行时异常的区别
编译时异常表示程序可能会出异常,但不代表一定会出异常。但是有出现异常的可能,所以编译器会要求对可能出现的异常进行处理才可以过编译。
throws
throws只会延迟异常处理,不会对异常进行实际处理,实际处理还是得靠try…catch…。
使用throws的意义:有些时候一个方法可能会出现异常,但没有能力处理异常。就好像开车的时候汽车除了异常,但汽车本身是没有办法做出处理的,只能交给驾驶汽车的人处理。在实际开发中经常是分工合作的,如果负责写某部分代码的人觉得该部分代码可能会出现异常/已经出现异常,就可以使用throws暂时延缓处理异常,等到负责其他部分代码的人调用到出现异常的代码后再进行具体处理。
自定义异常
有一些异常比如说考试成绩必须在0-100之间,这是java里没有的异常,所以我们需要自己定义。
throw后面跟的是异常对象名,eg:throw new ScoreException();
此处抛出的是一个ScoreException类的匿名对象。
自定义异常时,可以在抛出异常时(throw)调用自定义异常类的有参构造方法,将异常原因作为参数传给自定义异常类对象,这样在catch时调用printStackTrace()
时就会输出异常原因。(自定义异常类的有参构造方法调用了父类Throwable的有参构造方法,参数会赋值给成员变量detailMessage(继承自Throwable类),调用printStackTrace()
时输出的就是detail Message的值)
2.集合进阶
集合类的特点
提供一种存储空间可变的存储模型,存储的数据容量可以随时发生改变。
集合类体系结构
Collection
概述
Collection集合常用方法
tips:command + 7可以打开structure窗口,查看类的所有信息
boolean add(E e)
返回值永远是true,可以添加重复元素。(因为ArrayList是List的实现类,而List接口是允许出现重复元素的)
Collection集合的遍历
iterator()
方法返回的是Iterator接口的一个实现类
Collection集合遍历模版
Collection<String> c = new ArrayList<String>();
Iterator<String> it = c.iterator();
while (it.hasNext()){
String s = it.next();
System.out.println(s);
}
集合的使用步骤
List
List集合概述
List集合的特有方法
void add(int index, E element)
这里的索引需要在集合现有元素范围内,类似于数组的索引从0开始,如果索引超出了范围会报错IndexOutOfBoundsException
。其他涉及到索引的方法也类似。
并发修改异常
迭代器里的next()
方法会调用checkForComodification()
方法,而checkForComodification()
方法会对集合实际修改次数和预期修改次数进行判断,if (modCount != expectedModCount)
,如果二者不相等则抛出异常throw new ConcurrentModificationException();
。而ArrayList类里的add()
方法会让集合实际修改次数增加modCount++;
,而在Iterator接口的实现类Itr里定义了int expectedModCount = modCount
,也就是说这二者一开始是相等的。当modCount++
执行完之后二者就不等了,所以checkForComodification()
会抛出异常。
迭代器是通过cursor指针指向对应集合元素来挨个获取集合中元素的,每次获取对应元素后cursor值+1指向下一个元素,直到集合最后一个元素。那么如果在迭代器获取元素的过程中,集合中元素的个数突然改变,那么下一次获取元素时,cursor能否正确的指向集合的下一个元素就变得未知了,这种不确定性有可能导致迭代器工作出现意想不到的问题,为了出现这种现象,我们在使用迭代器时不能对集合结构进行直接修改(有其他修改方式,但不能通过集合直接增删元素),否则迭代器会抛出异常结束程序。
列表迭代器ListIterator
ListIterator继承自Iterator类,所以可以使用next()
和hasNext()
方法。
逆向遍历使用格式同正向遍历类似,但是并不常用。正向遍历一般用iterator()
实现,使用listIterator()
实现也很少用。
使用ListIterator里的add()
方法不会引发并发修改异常。因为在该add()
方法中,用这样一行代码expectedModCount = modCount
,把实际修改次数重新赋值给预期修改次数,这样在调用checkForComodification()
方法时就不会抛出异常。(Iterator里的add()
方法只有modCount++
,并没有进行重新赋值操作,所以会引发并发修改异常)。
增强for循环
如何验证增强for循环的内部是一个Iterator迭代器
for (String s : list) {
if (s.equals("world)) {
list.add("javaee")
}
}
执行本段代码时会抛出并发修改异常,证明了增强for循环的内部原理是一个Iterator迭代器。
数据结构
栈(具有LIFO的性质)
队列
数组
链表
List集合子类特点
LinkedList集合的特有功能
Set
Set集合概述和特点
Set是一个接口,不能直接实例化,还是得以多态的形式创建对象。
HashSet:对集合的迭代顺序不作任何保证。也就是说输出的顺序并不一定会和你添加元素的顺序一样。
哈希值
String类重写了hashCode()
方法,所以可能会出现不同对象的哈希值相同。eg:"重地"和“通话”的哈希值都是1179395,而“种地”和“通话”则不一样。
HashSet集合概述和特点
不能使用普通for进行遍历,但是可以使用迭代器和增强for进行遍历。(一般来说,集合的常用遍历方式就是迭代器、普通for和增强for)
因为HashSet是无序的,所以没有带索引的方法
HashSet集合保证元素唯一性源码分析
如何重写:在HashSet的存储对象对应的类里重写这两个方法,用command + N自动生成即可(模板选Intellij Idea,一路next下去)。
常见数据结构之哈希表
jdk8之前,底层实现为数组 + 链表,可以说是一个元素为链表的数组;jdk8以后,在长度比较长时底层实现了优化。(此处不讲解)
HashMap里的数组默认大小为16。
如何保证元素唯一性:数组中的元素的哈希值对16取余计算得在数组中的存储位置,多个元素存储在相同位置时,先比较哈希值,若哈希值不同则以链表的形式存储;若哈希值相同则比较元素具体内容,相同则不存储,不相同则存储。
LinkedHashSet集合概述和特点
TreeSet集合概述和特点
集合存储的只能是引用类型,如果要存储基本类型需要使用其对应的包装类型,eg:int——Integer
自然排序:经过测试可得,String类的自然排序是按照字符的ascii码由小到大进行排序,int的自然排序是按数字从小到大进行排序。
自然排序Comparable的使用
如果用TreeSet集合存储自定义对象,但是该对象的类里没有实现Comparable接口,那么会报错ClassCastException
类转化异常。原因是Comparable接口会对实现它的类强加一个整体排序(即自然排序),而TreeSet的无参构造方法会根据元素的自然排序进行排序,所以要想使用TreeSet集合存储自定义对象,自定义对象对应的类必须先实现Comparable接口并且重写CompareTo()
方法。
在重写compareTo()
方法时,返回值为0代表比较的二者相同,而Set集合不允许出现重复元素,所以调用compareTo()
方法的对象不会被添加进集合里。若返回值为正数,则代表调用该方法的对象较大,会将其按照升序的排序方式添加在被比较的对象之后;若返回值为负数,则调用compareTo()
方法的对象会被添加在被比较的对象之前。
TreeSet添加元素的逻辑:会让每一个试图加入集合的元素(即add()
方法的参数)调用compareTo()
方法,与所有已添加进集合的对象依次进行比较,已判断是否将该对象加入集合/该对象的存储位置。此时compareTo()
方法里,this指的是试图加入集合的新元素。
如何实现按照年龄从小到大排序,年龄相同时按照姓名的字母顺序排序:
@Override
public int CompareTo(Student s){
int num = this.age - s.age; //对年龄进行判断
int num2 = (num == 0 ? this.name.compareTo(s.name) : num); //若年龄相同则按照String类里重写的compareTo()方法进行比较
return num2;
}
重写compareTo()
方法通用模版:利用this和形参做减法。若打算按升序进行排列,则把this放在前面,若按降序排列则把this放在后面。
比较器排序Comparator的使用
如何通过比较器排序实现按照年龄从小到大排序,年龄相同时按照姓名的字母顺序排序:
TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>(){
@Override
public int compare(Student s1, Student s2){
int num1 = s1.getAge() - s2.getAge(); //这里的s1其实就是自然排序里的的compareTo()方法里的this
int num2 = (num1 == 0 ? s1.getName().compareTo(s2.getName()) : num1);
}
});
泛型
泛型的概述及好处
泛型的好处:如果没有使用泛型,可能会隐含类型转换异常ClassCastException
。当集合中存储多种类型的元素,而对集合中的对象进行强制类型转换,就会出现类型转换异常。
泛型类
使用泛型类的好处:可以将泛型作为方法的参数和返回值。
泛型方法
使用泛型方法的好处:在普通类里也可以定义泛型方法,可以实现同一个方法传入不同种类数据类型。
泛型接口
泛型接口对应的实现类如何定义:
public class GenericImpl<T> implements Generic<T> {
@Override
public void show(T t){
System.out.println(t);
}
}
类型通配符
List<?> list1 = new ArrayList<Object>();
List<?> list2 = new ArrayList<Number>();
List<?> list3 = new ArrayList<Integer>();
List<? extends Number> list4 = new ArrayList<Object>(); //报错,上限是Number类,Object类是Number类的父类
List<? extends Number> list5 = new ArrayList<Number>();
List<? extends Number> list6 = new ArrayList<Integer>();
List<? super Number> list7 = new ArrayList<Object>();
List<? super Number> list8 = new ArrayList<Number>();
List<? super Number> list9 = new ArrayList<Integer>(); //报错,下限是Number类,Integer类是Number类的子类
可变参数
注意事项:如果没把可变参数放在最后,而是放在最前面,那么可变参数会把所有传进来的参数都当作自己的参数封装进数组里,那么该方法后续的参数就接受不到对应的实参了。
利用可变参数实现多个数据求和:
public static int sum(int...a) {
int sum = 0;
for(int i : a) {
sum += i;
}
return sum;
}
可变参数的使用
通过Arrays.asList()
方法得到的List集合不能使用add()
和remove()
方法,会报错UnsupportedOperationException
,因为会改变该集合的大小,但是使用set()
方法对集合中的元素进行修改是可行的;通过List.of()
方法得到的List集合,增删改都不可以;通过Set.of()
方法得到的Set集合在给元素时不能给重复的元素(Set集合的特点:不包含重复元素),且得到的集合不能做增删操作,没有修改的方法(Set集合不包含带索引的方法)。
Map
Map集合的概述和使用
可以利用put()
方法添加元素/修改元素,如果键重复的话,该键添加进集合的值会覆盖该键之前对应的值。
Map集合的基本功能
Map集合的获取功能
Map集合的遍历
方式1:
方式2:
Collections
Collections概述和使用
IO流
File
File类概述和构造方法
三个构造方法可以做的事情是一样的,其中使用第一种构造方法最为简单方便。
File类创建功能
public boolean createNewFile()
如果文件不存在就创建文件并返回true,如果文件存在就不创建文件并返回false。
public boolean mkdir()
如果目录不存在就创建目录并返回true,如果目录存在就不创建目录并返回false。
public boolean mkdirs()
如果目录不存在就创建目录并返回true,如果目录存在就不创建目录并返回false。
***注意:不能根据路径名来判断是文件夹还是文件,而应该以具体调用的方法为准。***同一个文件夹下不允许存在两个同名的文件/文件夹,哪怕是一个是文件夹一个是文件也不可以,如果已经存在一个名为1.txt的文件夹,要再调用createNewFile()
创建一个名为1.txt的文件就会失败。
File类判断和获取功能
list()
返回的是字符数组,只是文件/目录的名称,而listFiles()
返回的是File对象,可以调用File类里的方法。
File类删除功能
递归
递归内存图
字节流
IO流概述和分类
输入流:从硬盘中读取数据存进内存;输出流:从内存中读取数据写进硬盘。
字节流写数据
所有和io相关的操作最后都要释放资源。
调用字节输出流对象的wirte()
方法往文件里写数据,写的是97,在文件里查看的时候是小写字母a,因为我们使用的是字节流写数据,字节流对应的是计算机的机器语言,所以这里的97不是我们常识认知里的数字97,而是计算机世界里的ascii码97。
字节流写输入的三种方法
FileOutputStream类
有两个构造方法,一个是以文件为参数,一个是以字符串为参数,这二者其实做的是同一件事。
FileOuuputStream(String name)
的底层实现会对传入的路径进行判断,如果为空则会新建一个文件。所以FileOuuputStream(String name)
和FileOuuputStream(File file)
其实做的都是同一件事。
在调用write(byte[] b)
方法时,该字节数组可以通过String类里的getBytes()
方法得到字符串对应的字节数组。
字节流写数据实现换行&追加写入
字节流写数据异常处理
如果没有finally块的话,在执行释放资源的语句之前就出现异常,那程序就会跳转去执行catch代码块,这样资源就一直得不到释放。
如何加入finally来实现释放资源
文件输出流对象的定义要被放在整个try…catch…finally代码块外,确保之后的所有代码块都可以正常地访问该对象。创建的时候要先初始化(赋初值);如果新建对象的动作没有成功执行(如路径错误等),后续调用close()
方法时会出现空指针异常,所以在finally里为了保证代码的健壮性要对文件输出流对象进行判空操作。
FileOutputStream fos = null;
try {
fos = new FileOutputStream("myByteStream\\fos.txt");
fos.write("hello".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
字节流读数据(一次读一个字节数据)
读取数据(一次读一个字节数据)标准代码:while循环里其实做了三件事,分别是读数据fis.read()
、把读取到的数据赋值给byby=fis.read()
、判断读取到的数据是否是-1(如果已经读取到文件末尾,read()
的返回值是-1by != -1
)。
FileInputStream fis = new FileInputStream("myByteStream\\fos.txt"); //创建字节输入流对象
int by;
while (by = fis.read() != -1) {
System.out.print((char)by);
}
fis.close; //释放资源
字节流读数据(一次读一个字节数组数据)
读取数据(一次读一个字节数组数据)标准代码:read()
方法返回的是实际读取到的字节数,若返回值为-1则说明文件已被读取完。
FileInputStream fis = new FileInputStream("myByteStream\\fos.txt"); //创建字节输入流对象
byte bys = new byte[1024]; //byte数组大小为1024及其整数倍
int len;
while (len = fis.read(bys) != -1) {
System.out.print(new String(bys, 0, len));
}
fis.close; //释放资源
字节缓冲流
使用模板:
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("myByteStream\\bos.txt"));
BufferedInputStream bis = new BufferedinputStream(new FileInputStream("myByteStream\\bos.txt"));
读写速度:字节缓冲流一次读写一个字节数组 > 字节缓冲流一次读写一个字节 > 基本字节流一次读写一个字节数组 > 基本字节流一次读写一个字节
字符流
为什么出现字符流
UTF-8编码下,一个汉字占3个字节;GBK编码下,一个汉字占2个字节。
编码表
字符串中的编码解码问题
字符流中的编码解码问题
字符流写数据的5种方式
使用字符流写数据的问题,如果不调用flush()
方法进行刷新,数据会被暂存在缓冲区里,是写不进文件里的。close()
方法是关闭流,但是关闭之前会先刷新流,即close()
方法底层会调用一次flush()
方法。
字符流读数据的两种方式
字节流读数据和字符流读数据两个方法的调用格式是一样的,只不过一个是字节流一个是字符流。
字符缓冲流
字符缓冲流特有功能
BufferedWriter写数据模板:
BufferedWriter bw = new BufferedWriter(new FileWriter("myCharStream\\bw.txt"));
for (int i = 0; i < 10; i++) {
bw.write("hello" + i);
bw.newline();
bw.flush();
}
bw.close();
BufferedReader读数据模板:
BufferedReader br = new BufferedReader(new FileReader("myCharStream\\bw.txt"));
String line;
while ((line =br.readline()) != null) {
System.out.println(line);
}
br.close();
IO流小结
复制文件的异常处理
从某种程度上来说jdk7的改进方案是最优解,虽然相比于jdk9的方案略显繁琐,但是jdk9的方案里还是用到了throws处理异常的方法,而我们用try…catch…finally的目的就是为了当场解决异常,不用等到调用的时候再解决。
特殊操作流
标准输入流
字节流:多态的形式创建对象
InputStream is = System.in;
用转换流把字节流转为字符流:
InputStreamReader isr = new InputStreamReader(is);
为了实现一次读取一行数据(字符缓冲输入流的特有方法),用字符缓冲输入流包装:
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
标准输出流
输出语句的本质是一个标准的输出流,PrintStream类有的方法,System.out都可以使用。
PrintStream ps = System.out;
字节打印流
字符打印流
对象序列化流
对象反序列化流
serialVersionUID & transient
抛出InvalidClassException
异常的原因:类的串行版本与从流中读取的类描述符的类型不匹配。每一个类中都有一个变量serialVersionUID
,当你修改类里面的内容时,这个UID就会变化一次。如果对象序列化流文件里记录的UID与文件实际的UID不匹配,就会抛出该异常。
Properties
Properties使用时不用指定类型
eg:Properties prop = new Properties();
Properties作为Map集合的特有方法
serProperty()
和put()
的区别:put()
接收的是Obejct类型的参数,而setProperty()
只能接受String类型的参数
使用以上三个方法可以遍历集合:setProperty()
设置键和值,stringPropertyNames()
返回键集,增强for遍历键集后使用getProperty()
返回对应的值。
Properties和IO流相结合的方法
store()
的第二个参数是描述信息,如果没有任何要描述的就写null即可。