目录
1、java语言有哪些优点和缺点?
2、JVM 、 JDK 和 JRE的关系
3、为什么说 Java 语言“编译与解释并存”?
4、Java和c++的区别
5、基本数据类型
5.1、java的8种基本数据类型:
5.2、基本类型和包装类型的区别:
5.3、包装类型的缓存机制:
5.4、自动装箱和自动拆箱:
5.5、浮点数运算的精度丢失问题及解决办法:
6、变量
6.1、成员变量与局部变量的区别?
6.2、静态变量有什么作用?
6.3、字符型常量和字符串常量的区别?
7、方法
7.1、静态方法和实例方法的区别:
7.2、静态方法为什么不能调用非静态成员?
7.3、重载和重写的区别:
8、面向对象基础
8.1、面向对象和面向过程的区别:
8.2、创建一个对象用什么运算符?对象实体与对象引用有何不同?
8.3、对象的相等和引用相等的区别:
8.4、构造函数是什么?能否被override?类没有声明构造方法,程序还能正确执行吗?
8.5、面向对象三大特征
8.6、接口和抽象类有什么共同点和区别?
8.7、深拷贝和浅拷贝的区别?什么是引用拷贝?
9、Object类
9.1、Object 类的常见方法有哪些?
9.2、== 和 equals() 的区别:
9.3、为什么重写 equals() 时必须重写 hashCode() 方法?
10、String类
10.1、String 为什么是不可变的?
10.2、String、StringBuffer、StringBuilder 的区别:
10.3、String s1 = new String("abc");这句话创建了几个字符串对象?
10.4、String#intern 方法有什么作用?
10.5、String 类型的变量和常量做“+”运算时发生了什么?
11、异常
11.1、Exception 和 Error 的区别:
11.2、Checked Exception 和 Unchecked Exception的区别:
11.3、Throwable 类常用方法有哪些?
11.4、try-catch-finally 如何使用?
11.5、finally 中的代码一定会执行吗?
11.6、如何使用 try-with-resources 代替try-catch-finally?
12、泛型的使用方式有哪几种?
13、反射
13.1、反射的使用:
13.2、反射的应用场景:
14、注解的解析方法有哪几种?
15、什么是序列化?什么是反序列化?
16、I/O流
字节缓冲流:
Java 中 3 种常见 IO 模型:
1、java语言有哪些优点和缺点?
优点:
- 面向对象(封装,继承,多态)
- 平台无关性( Java 虚拟机实现平台无关性)
- 支持多线程
- 具备异常处理和自动内存管理机制
- 支持网络编程并且很方便
- 编译与解释并存
缺点:
- 使用大量的内存:垃圾回收机制需要占用大量内存,可能导致程序的运行速度相对较慢。
- 不支持底层操作:Java使用虚拟机来实现,无法和操作系统的底层打交道了。
- 启动时间慢:由于Java程序需要先启动虚拟机,再执行程序,启动时间相对较慢。
2、JVM 、 JDK 和 JRE的关系
- JDK(Java Development Kit):是 Java 开发工具包,是整个 Java 的核心,它包含了JRE,同时还包含了 javac(编译 java 源码的编译器)、jconsole(基于 JMX 的可视化监控⼯具)、javap(反编译工具)等等。
- JRE( Java Runtime Environment):是 Java 的运行环境,包含 JVM 及 Java 核心类库。
- JVM(Java Virtual Machine):Java 虚拟机,是整个 Java 实现跨平台的最核心的部分。所有 Java 程序会首先被编译为 .class 的类文件,这种类文件可以在虚拟机上执行。
3、为什么说 Java 语言“编译与解释并存”?
高级编程语言按照程序的执行方式分为两种:
- 编译型语言:通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
- 解释型语言:通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。一般开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。
Java 语言既具有编译型语言的特征,又有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class
文件),这种字节码必须由 Java 解释器来解释执行。
4、Java和c++的区别
- Java 不提供指针直接访问内存,程序内存更加安全。
- Java 的类单继承的,C++ 支持多重继承;虽然 Java 类不支持多继承,但接口支持多继承。
- Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
- C++支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。
- ……
5、基本数据类型
5.1、java的8种基本数据类型:
5.2、基本类型和包装类型的区别:
- 用途:基本类型一般用来定义一些局部变量和常量,在其他地方比如方法参数、对象属性中一般使用包装类型,并且包装类型可用于泛型,而基本类型不可以。
- 存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量存放在 Java 虚拟机的堆中。包装类型属于对象类型,放在堆中。
-
占用空间:相比于包装类型, 基本数据类型占用的空间比较小。
-
比较方式:对于基本数据类型来说,
==
比较的是值。对于包装数据类型来说,==
比较的是对象的内存地址。整型包装类对象之间值的比较,需要使用equals()
方法。
对于不同类型变量的存储问题,有两条黄金法则:
- 引用类型总是被分配到“堆”上。不论是成员变量还是局部
- 基础类型总是分配到它声明的地方:成员变量在堆内存里,局部变量在栈内存里。
public class Test {
// 成员变量,存放在堆中
int a = 10;
// 被 static 修饰,也存放在堆中,但属于类,不属于对象
// JDK1.7 静态变量从永久代移动了 Java 堆中
static int b = 20;
public void method() {
// 局部变量,存放在栈中
int c = 30;
static int d = 40; // 编译错误,不能在方法中使用 static 修饰局部变量
}
}
5.3、包装类型的缓存机制:
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
或 False,
两种浮点数类型的包装类 Float
,Double
没有实现缓存机制。
看如下代码:
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);
上述代码应输出false。首先第一行代码会发生自动装箱,因此变量i1使用的是缓存中的对象,而i2会新new一个对象出来,故两者不相等。
如果想比较包装类对象之间值,需要使用equals。
5.4、自动装箱和自动拆箱:
- 自动装箱:将基本类型用它们对应的引用类型包装起来;
- 自动拆箱:将包装类型转换为基本数据类型;
装箱其实就是调用了 包装类的valueOf()
方法,拆箱其实就是调用了 xxxValue()
方法。
因此,
Integer i = 10
等价于Integer i = Integer.valueOf(10)
int n = i
等价于int n = i.intValue()
;
5.5、浮点数运算的精度丢失问题及解决办法:
由于底层保存浮点数的机制,会出现小数的精度发生损失的情况,可以使用 BigDecimal
对浮点数的运算,不会造成精度丢失。如:
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
System.out.println(x); /* 0.1 */
System.out.println(y); /* 0.1 */
System.out.println(Objects.equals(x, y)); /* true */
6、变量
6.1、成员变量与局部变量的区别?
- 语法形式:
- 成员变量属于类,而局部变量是方法中定义的变量或参数;
- 成员变量可以被
public
,private
,static
等修饰符所修饰,而局部变量不能被访问控制修饰符及static
所修饰; - 成员变量和局部变量都能被
final
所修饰。
- 存储方式:
- 如果成员变量是使用
static
修饰的,那么这个成员变量是属于类的,如果没有使用static
修饰,这个成员变量是属于类实例的。 - 而对象存在于堆内存,局部变量则存在于栈内存。
- 如果成员变量是使用
6.2、静态变量有什么作用?
静态变量也就是被 static
关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建了多个对象,这样可以节省内存。
通常情况下,静态变量会被 final
关键字修饰成为常量。
6.3、字符型常量和字符串常量的区别?
- 形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。
- 含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。
- 占内存大小:字符常量占 2 个字节; 字符串常量占若干个字节。
7、方法
7.1、静态方法和实例方法的区别:
- 调用方式:在外部调用静态方法时,可以使用
类名.方法名
的方式,也可以使用对象.方法名
的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象 。 - 访问限制:静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。
7.2、静态方法为什么不能调用非静态成员?
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
- 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
7.3、重载和重写的区别:
- 重载:同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。
- 重写:子类对父类方法的重新改造,外部样子不能改变,内部实现逻辑可以改变。
8、面向对象基础
8.1、面向对象和面向过程的区别:
- 面向过程:把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
- 面向对象:是一种“万物皆对象”的编程思想,任何物体都可以归为一类事物,而每一个个体都是一类事物的实例,通过调用对象的方法来解决问题。
8.2、创建一个对象用什么运算符?对象实体与对象引用有何不同?
用new 运算符,new 创建对象实例,对象实例在堆内存中;对象引用指向对象实例,存放在栈内存中。
- 一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);
- 一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
8.3、对象的相等和引用相等的区别:
- 对象相等一般比较的是内存中存放的内容是否相等。
- 引用相等一般比较的是指向的内存地址是否相等。
例如:
String str1 = "hello";
String str2 = new String("hello");
String str3 = "hello";
// 使用 == 比较字符串的引用相等
System.out.println(str1 == str2);
System.out.println(str1 == str3);
// 使用 equals 方法比较字符串的相等
System.out.println(str1.equals(str2));
System.out.println(str1.equals(str3));
上述代码执行结果为:
结果分析:由于==比较的是引用相等,str2字符串会重新new一个string对象,因此引用和str1不同;str3会优先去字符串常量池寻找有无“hello”这个字符串,有的话就直接引用它,所以str3和str1引用相等。而最后两个比较的是字符串内容是否相等,故str1、str2、str3都相等。
8.4、构造函数是什么?能否被override?类没有声明构造方法,程序还能正确执行吗?
构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。构造函数可以被重载(overload),但不能被重写(override)。
当一个类没有声明构造方法,程序也是可以正常执行的。 当没有显式声明构造方式时,类会默认声明一个无参构造函数。但如果我们自己添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参构造方法了。
8.5、面向对象三大特征
- 封装:把数据和操作数据的方法封装起来,对数据的访问只能通过已定义的接口。
- 可以类比平常生活的开车,汽车的底层结构被封装起来,我们无需关心底层,只需要会踩油门、转动方向盘开车即可。
- 继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类,得到继承信息的被称为子类。
- 父类中的私有属性和方法子类是无法访问,只是拥有。
- 多态:一个对象具有多种状态,具体表现为父类的引用指向子类的实例。
- 多态存在的3个必要条件: 要有继承关系 、要有方法的重写、父类引用指向子类对象。
8.6、接口和抽象类有什么共同点和区别?
抽象类和普通类类似,只是无法实例化,存在的意义就是让其他类继承的。抽象类可以包含抽象方法和非抽象方法,其中抽象方法没有具体的实现,而非抽象方法有具体的实现代码。接口是一种定义了一组方法签名的集合,这些方法默认都是公共的抽象方法,不包含具体的实现代码,比抽象类更加抽象。
共同点:
- 都不能被实例化。(存在的意义就是被继承或实现)
- 都包含抽象方法。(抽象类也可以没有抽象方法,只要有abstract关键字修饰,但是没意义)
不同点:
- 用途:
- 接口主要用于限制类的行为,实现了某个接口就具有了对应的行为。
- 抽象类主要用于代码复用。
- 抽象类中可以定义构造函数,接口不能定义构造函数;
- 一个类只能继承一个类,但是可以实现多个接口。
8.7、深拷贝和浅拷贝的区别?什么是引用拷贝?
- 浅拷贝:只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
- 深拷贝:会创造一个一模一样的对象,新旧对象不共享内存,修改新对象不会影响到原对象。
9、Object类
9.1、Object 类的常见方法有哪些?
Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
/**
* native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
*/
public final native Class<?> getClass()
/**
* native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
*/
public native int hashCode()
/**
* 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
*/
public boolean equals(Object obj)
/**
* native 方法,用于创建并返回当前对象的一份拷贝。
*/
protected native Object clone() throws CloneNotSupportedException
/**
* 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
*/
public String toString()
/**
* native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
*/
public final native void notify()
/**
* native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
*/
public final native void notifyAll()
/**
* native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
*/
public final native void wait(long timeout) throws InterruptedException
/**
* 多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。。
*/
public final void wait(long timeout, int nanos) throws InterruptedException
/**
* 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
*/
public final void wait() throws InterruptedException
/**
* 实例被垃圾回收器回收的时候触发的操作
*/
protected void finalize() throws Throwable { }
9.2、== 和 equals() 的区别:
- “==”:由于 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。因此 == 对于基本数据类型来说比较的是值,对于引用数据类型来说比较的是内存地址。
- equals():如果类没有重写equals方法,等价于“==”的效果;如果重写了,则比较的是对象的属性是否相等。
9.3、为什么重写 equals() 时必须重写 hashCode() 方法?
如果不同时重写的话,可能会出现以下情况:set集合中出现两个相同的对象,即无法去重。
原因:set集合在进行去重操作时,会先判断两个对象的hashcode是否相等,如果不重写hashcode()方法,就会调用object中的hashcode()方法,返回的是对象内存地址的哈希码,这就意味着两个内容相同的对象,其哈希码会由于内存地址的不同而不同。 两个内容相同的对象哈希码不同,则默认这两个对象不同,都会插入到set集合中,导致set集合有重复对象。
10、String类
10.1、String 为什么是不可变的?
- 字符串常量池:
- Java内存中存在一个字符串常量池,是存储字符串对象的特殊内存区域。当创建一个字符串时,如果该字符串已经存在于字符串常量池中,那么就会直接返回这个字符串的引用;如果不存在,则会将该字符串添加到字符串常量池中,并返回新创建的字符串的引用。由于字符串常量池的存在,多个字符串可以共享同一个实例,这样可以节省内存空间。而如果 String 是可变的,那么在修改字符串时,会导致其他使用相同字符串的地方出问题。
- 安全性:
- 由于String不可变,因此其在多线程环境下是安全的。
- 缓存哈希值:
- String 类重写了 hashCode()方法,并且在第一次调用时计算并缓存了字符串的哈希值。由于 String 是不可变的,所以可以保证哈希值的唯一性和一致性。
10.2、String、StringBuffer、StringBuilder 的区别:
- 可变性:
- String 用于表示字符串,属于不可变类。所以每次对String进行修改都会创建一个新的String对象,导致频繁的内存分配和回收,影响性能。
- Java提供了StringBuffer 和 StringBuilder两个可变的字符串,用于高效地进行字符串的操作。
- 原理:
- String 内部使用字符数组来保存字符串内容,并且该数组是 final 修饰的,当对 String 进行修改时,会创建一个新的 String 对象;
- StringBuffer 内部也使用字符数组来保存字符串内容,但是该数组没有被 final 修饰,当对 StringBuffer 进行修改时,直接在原有的字符数组上进行操作,不会创建新的对象。
- StringBuilder 的实现原理与 StringBuffer 相同,唯一的区别是 StringBuilder 不是线程安全的,而 StringBuffer 是线程安全的。
10.3、String s1 = new String("abc");这句话创建了几个字符串对象?
分情况:
- 如果字符串常量池无该对象:那么会堆中创建两个字符串对象,一个存在字符串常量池中。
- 如果字符串常量池有该对象:那么只会在堆中创建一个字符串对象。
10.4、String#intern 方法有什么作用?
intern 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
10.5、String 类型的变量和常量做“+”运算时发生了什么?
当字符串不加 final
关键字拼接时:
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
执行结果分析:首先明确一点:
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
因此,上述代码中,除了str4的其余字符串都已经存入到字符串常量池中;而str4涉及到两个字符串的拼接,底层会在堆中创建一个新的对象,所以str4和其他字符串的比较都为false。
不过,当字符串使用 final
关键字声明之后,可以让编译器当做常量来处理:
final String str1 = "str";
final String str2 = "ing";
// 下面两个表达式其实是等价的
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true
11、异常
Java 异常类层次结构图概览:
11.1、Exception 和 Error 的区别:
- Exception:程序本身可以处理的异常,可以通过catch来获取,Exception又可以细分为受检查异常和不受检查异常。
- Error:属于程序无法处理的错误。
11.2、Checked Exception 和 Unchecked Exception的区别:
- Checked Exception:即受检查异常,Java 代码在编译过程中,如果受检查异常没有被
catch
或者throws
关键字处理的话,就没办法通过编译。除了RuntimeException
及其子类以外,其他的Exception
类及其子类都属于受检查异常 。常见的受检查异常有:IO 相关的异常、ClassNotFoundException
、SQLException
...。 - Unchecked Exception:即不受检查异常,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
RuntimeException
及其子类统称为不受检查异常,常见的有:NullPointerException
(空指针错误)ArrayIndexOutOfBoundsException
(数组越界错误)ClassCastException
(类型转换错误)IllegalArgumentException
(参数错误比如方法入参类型错误)NumberFormatException
(字符串转换为数字格式错误)ArithmeticException
(算术错误)SecurityException
(安全错误比如权限不够)UnsupportedOperationException
(不支持的操作错误比如重复创建同一用户)
11.3、Throwable 类常用方法有哪些?
String getMessage()
: 返回异常发生时的简要描述String toString()
: 返回异常发生时的详细信息String getLocalizedMessage()
: 返回异常对象的本地化信息。使用Throwable
的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()
返回的结果相同void printStackTrace()
: 在控制台上打印Throwable
对象封装的异常信息
11.4、try-catch-finally 如何使用?
- try块:用于捕获异常。其后可接零个或多个
catch
块,如果没有catch
块,则必须跟一个finally
块。 - catch块:用于处理 try 捕获到的异常。
- finally 块:无论是否捕获或处理异常,
finally
块里的语句都会被执行。当在try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行。
11.5、finally 中的代码一定会执行吗?
不一定,在某些情况下,finally 中的代码不会被执行,如:
- 在执行finally的代码之前虚拟机被终止了。
- 程序所在的线程死亡。
- 关闭CPU。
11.6、如何使用 try-with-resources 代替try-catch-finally?
try-with-resources 是 Java 7 引入的一种语法结构,用于自动关闭实现了 AutoCloseable
接口的资源。它可以代替传统的 try-catch-finally 结构来处理资源的释放。
优点:
- 简化了代码结构:不再需要显式地编写 finally 块来关闭资源。
- 自动确保资源的正确关闭:无论是否发生异常,都会自动调用资源的
close()
方法进行关闭。 - 支持多个资源同时关闭:可以在 try 后面的括号内声明多个资源,并按照声明顺序逆序关闭。
12、泛型的使用方式有哪几种?
使用泛型,可以增加代码的灵活性和安全性,使得代码更加通用和易于维护。
- 泛型类:定义一个类时,可以在类名后面加上尖括号<>,并在其中指定类型参数。
- 泛型接口:与泛型类类似,可以在接口名后面加上尖括号<>,并在其中指定类型参数。然后在实现该接口时,需要指定具体的类型。
- 泛型方法:即在方法返回值前面加上尖括号<>,并在其中指定类型参数。
13、反射
13.1、反射的使用:
反射是java语言的一个特性,它允程序在运行时动态获取和调用类的属性和方法。
要使用反射,需要先获取类的class对象(字节码对象),有三种方式:
- Class.forName("全类名");
- 类名.class;
- 类实例对象.getClass();
接下来就可以通过具体的API调用获取到详细的属性或者方法:
- 获取构造函数:getConstructors()等方法。
- 获取类成员变量:getFields()等方法。
- 获取类成员方法:getMethods()等方法。
13.2、反射的应用场景:
Spring 框架的 IOC(动态加载管理 Bean)、加载数据库驱动:
Class.forName("com.mysql.cj.jdbc.Driver");
、IDEA的方法提醒都需要用到反射机制。
14、注解的解析方法有哪几种?
注解只有被解析之后才会生效,常见的解析方法有两种:
- 编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用
@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 - 运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的
@Value
、@Component
)都是通过反射来进行处理的。
15、什么是序列化?什么是反序列化?
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
简单来说:
- 序列化:将数据结构或对象转换成二进制字节流的过程。
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程。
序列化协议对应于OSI 七层协议模型中表示层。
对于不想进行序列化的变量,使用 transient
关键字修饰。
transient
只能修饰变量,不能修饰类和方法。static
变量不属于任何对象(Object),所以无论有无transient
修饰,均不会被序列化。
16、I/O流
I/O即Input/Output,数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
Java I/O流中有四个抽象类基类,其架构如下图:
字节流适合传输所有类型的文件,如音频、视频、图片、文本文件的复制转移等。字符流只适合传输纯文本文件,如txt、java文件等。
字节缓冲流:
字节流在调用 write(int b)
和 read()
方法的时候,一次只能读取一个字节。由于字节缓冲流内部有缓冲区(字节数组),因此,字节缓冲流会先将读取到的字节存放在缓存区,大幅减少 IO 次数,提高读取效率。
Java 中 3 种常见 IO 模型:
-
BIO (Blocking I/O):同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。在高并发场景下效率较低。
-
NIO (Non-blocking I/O):同步非阻塞 IO 模型,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。通过轮询操作,避免了一直阻塞,但是不断发起调用消耗大量cpu资源。
-
AIO (Asynchronous I/O):异步 IO 模型,应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。