JavaGuide知识点整理——基础常见知识点(下)
异常
Exception和Error有什么区别?
在java中,所有的异常都有一个共同的祖先,java.lang包中的Throwable类。
而这个Throwable有两个重要的子类:
- Exception:程序本身可以处理的异常,可以通过catch来进行捕获。
- Error:Error属于程序无法处理的错误。不建议通过catch来捕获。例如java虚拟机运行错误,虚拟机内存不够错误(OutOfMemoryError),类定义错误(NoClassDefFoundError)等,这些异常发生时,java虚拟机一般会选择线程终止。
Checked Exception 和 Unchecked Exception 有什么区别?
-
Checked Exception 是受检查异常,java代码在编写过程中如果受检查异常没有被catch或者throws关键字处理的话,就没办法通过编译。
比如sleep异常,还有常见的io异常等,都必须主动处理。 - Unchecked Exception 是不受检查异常,java代码在编写过程中,我们即便不处理也可以正常通过编译的。
RuntimeException以及其子类都是非受检查异常,常见的有:
- NullPointerException 空指针错误
- IllegalArgumentException 参数错误比如方法入参类型错误
- NumberFormatException 字符串转化成数字错误
- ArrayIndexOutOfBoundsException 数组下标越界错误
- ClassCastException 类型转换错误
- ArithmeticException 算数错误
...
Throwable 类常用方法有哪些?
- String getMessage():返回异常发生时的简要描述
- String toString(): 返回异常发生时的详细信息
- StringgetLocalizedMessage(): 返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以生成本地化信息,如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同。
- void printStackTrace():在控制台上打印Throwable对象封装的异常信息
try-catch-finally如何使用?
- try块:用于捕获异常,其后面可以接0个或者多个catch块,如果没有catch块必须接一个finally块。
- catch块:用于处理try捕获到的异常。
- finally块:无论是否捕获或者处理异常,finally块里的语句都会被执行。当在try块或者catch块中遇到return语句时,finally语句块将在方法返回之前被执行。
注意:如果finally的语句块中也有return语句,那么之前在try块中的return语句或者catch块中的return语句都会被覆盖。
finally中的代码一定会执行么?
不一定的,在某些情况下finally中的代码不会被执行。比如说finally之前虚拟机被终止运行的话(System.exit(1);),finally中的代码就不会被执行。
下面两种情况finally中的代码也不会被执行:
- 程序所在的线程死亡
- 关闭cpu。
如何使用try-with-resources 代替try-catch-finally?
首先说一下try-with-resources的用法:
try (Scanner scanner = new Scanner(new File("test.txt"))) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}
我们可以看到是try后面接代码块,如果这个代码块正常执行会进入到try块。如果代码块有问题直接会进入到catch。
优点是写法简单,而且逻辑清晰。更容易编写必须要关闭的资源的代码。
但是也有个问题:很多时候我们用try并不是为了保住某一个语句,而是一个完整的代码块:比如说自己写导出/导出excel的时候,每一行都有可能出现格式错误/内容错误等,所以这种用try-with-resources就不是很好了。我们应该自己酌情使用两者。
异常使用有哪些需要注意的地方?
- 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱,每次手动抛出异常,我们都需要手动new 一个异常对象抛出。
- 抛出的异常信息一定要有意义。
- 建议抛出更具体的异常,比如字符串转换为数字错误,我们应该抛出NumberFormatException而不是Exception.
- 使用日志打印异常之后就不要再抛出异常了(两者不要同时存在一段代码逻辑中)
泛型
什么是泛型?有什么作用?
java泛型是jdk5中引入的一个新特性,使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如ArrayList<User> users = new ArrayList<User>() 这行代码指明users只能传入User对象。如果传入其他类型的对象就会报错。
泛型的使用方式有哪几种?
泛型一般有三种使用方式:泛型类,泛型接口,泛型方法。
泛型类demo:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
实例化泛型类的时候:
Generic<Integer> genericInteger = new Generic<Integer>(123456);
泛型接口demo:
public interface Generator<T> {
public T method();
}
实现泛型接口不指定类型:
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
实现泛型接口指定类型:
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
泛型方法demo:
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
泛型方法的使用:
// 创建不同类型数组: Integer, String
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray );
printArray( stringArray );
项目中哪里用到了泛型?
- 自定义接口通用返回结果中,一般data都用泛型接收的
- 定义Excel处理类的时候,一般传入的数组也都是用泛型接收
- 构建集合工具类等
反射
何为反射?
如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定会对反射这个概念不陌生。
反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中的方法的能力。通过反射可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
反射机制优缺点
- 优点:可以让代码更加灵活,为各种框架提供开箱即用的功能提供了便利。
- 缺点:让我们在运行时有了分析操作类的能力。这样同时也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外反射的性能也稍微差点。
反射的应用场景
平时大部分时候都在写业务代码的话,很少会接触到直接使用反射机制的场景。
但是这并不代表反射没有用。相反,正是因为反射,你才能这么轻松的使用各种框架,像是spring,springboot,mybatis等框架都大量使用了反射机制。
这些框架中大量使用了动态代理,而动态代理的实现也依赖反射。
另外java中的一大利器 注解 的实现也用到了反射。
为什么使用spring的时候一个@Component注解就声明了一个类为spring bean?
为什么通过一个@Value注解就可以读取到配置文件中的值呢?这些都是因为可以基于反射分析类,然后获取类/属性/方法/方法的参数上的注解,获取到注解后做进一步的处理。
注解
注解是java5开始引入的新特性。可以看作是一种特殊的注释。主要用于修饰类,方法,或者变量。
注解本质是一个继承了Annotation的特殊接口。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
public interface Override extends Annotation{
}
注解只有被解析之后才会生效,常见的解析方法有两种:
- 编译期直接扫描:编译器在编译java代码的时候扫描注解并处理。比如某个方法使用@Override注解,编译器在编译的时候会检测当前方法是否重写了父类的对应的方法。
- 运行期通过反射处理:像是框架中自带的注解都是通过反射来进行处理的。
JDK提供了很多内置的注解,比如@Override,@Deprecated.同时我们还可以自定义注解。
I/O
什么是序列化?什么是反序列化?
如果我们需要持久化java对象,比如将java对象保存在文件中,或者网络传输java对象,这些场景都需要用到序列化、
简单来说:
- 序列化:将数据结构或者对象转化成二进制字节流的过程。
- 反序列化:将在序列化过程中所生成的二进制字节流转化成数据结构或者对象的过程。
对于java这种面向对象的编程语言来说,我们序列化的都是对象,也就是实例化后的类,但是在C++这种半面向对象的语言中,struct定义的是数据结构类型。而class对应的是对象类型。
综上,序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统,数据库,内存中
java序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,可以使用transient关键字修饰、
transient关键字的作用:阻止实例中那些用此关键字修饰的变量序列化。当对象被反序列化时,被此关键字修饰的变量值不会被持久化和恢复。
关于transient还有几点注意:
- transient只能修饰变量,不能修饰类和方法
- transient修饰的变量,在反序列化后变量值将会被置成类型的默认值,例如如果是int类型,那么反序列化后值是0.
- static变量不属于任何对象,所以无论有没有transient关键字修饰,都不会被序列化。
获取键盘输入的常用的两种方式
- 通过Scanner
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();
- 通过BufferedReader
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
Java中IO流分为几种?
- 按照流的流向分,可以分为输入流和输出流
- 按照操作单元划分,可以分为字节流和字符流
- 按照流的角色划分,可以分为节点流和处理流
Java IO流共40多个类,这些类看上去很杂乱,但是实际上很有规划,而且彼此之间联系也很紧密,java IO流的40多个类都是从如下4个抽象类中派生出来的:
-
InputStream/Reader:所有输入流的基类,前者是字节输入流,后者是字符输入流。
-
OutputStream/Writer:所有输入流的基类,前者是字节输出流,后者是字符输出流。
- 按照操作方式分类: IO-操作方式分类.png
-
按照操作对象分类:
IO-操作对象分类.png
既然有了字节流,为什么还要有字符流?
不管是文件读写还是网络发送接受,信息的最小存储单元都是字节,为什么i/o流操作要分为字节流操作和字符流操作?
字符流是有java虚拟机将字节转换得到的,问题就出现在这个过程是非常耗时,并且如果我们不知道编码类型很容易就出现乱码问题。所以I/O流干脆就提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件,图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。