Java
数据类型
Q1:基本类型和包装类型的区别?
- 用途:在对象属性中一般用包装类型,包装类型可用于泛型,基本类型不可以
- 存储方式:包装类型一般放在堆,基本数据类型的局部变量一般放在栈中的局部变量表,未被static修饰过的成员变量一般放在堆中
- 占用空间:包装类型占用空间更大
- 默认值:包装类型默认都为null,基本数据类型不是null
- 比较方式:基本数据类型
==
比较的是值,包装类型==
比较的是内存地址,整形包装类对象之间值的比较一般用equals()
Q2:包装类型的缓存机制
Byte
,Short
,Integer
,Long
默认创建了数值[-128,127]的相应类型缓存数据,Character
创建了数值在[0,127]范围的缓存数值,Boolean
直接返回True/False
对于Integer var=?
在-128和127之间的赋值,Integer
对象是在IntegerCache.cache
产生,会复用已有对象,这个区间内的Integer
的值可以直接用==判断,但是这个区间之外的所有数据,都会在堆上产生,不复用已有对象,使用equals
方法。
Q3:自动装箱与拆箱了解吗?
- 装箱:将基本数据类型用它们对应的引用类型包装起来
- 拆箱:将包装类型转换为基本数据类型
举例:
Integer i = 10; //装箱 等价于 Integer.valueOf(10)
int n= i; //拆箱 等价于 i.intValue()
关键字
Q1:final关键字的作用
- 修饰变量:将变量声明为
final
后,该变量的值不能再被修改,成为常量。常量在声明后必须进行初始化,并且不能再被重新赋值。 - 修饰方法:将方法声明为
final
后,该方法不能被子类重写。final
方法在父类中已经实现了最终的功能,子类无法修改它。 - 修饰类:将类声明为
final
后,该类不能被继承。final
类不能有子类,它是最终的实现。
变量
Q1:成员变量与局部变量的区别?
-
语法形式:成员变量是属于类的,局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被
public
,private
,static
等修饰符所修饰,而局部变量不能被访问控制修饰符及static
所修饰;但是,成员变量和局部变量都能被final
所修饰。 -
存储方式:如果成员变量是使用
static
修饰的,那么这个成员变量是属于类的,如果没有使用static
修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 -
生存时间:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
-
默认值:成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被
final
修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
方法
Q1:访问类成员是否存在限制?
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。
Q2:重载和重写有什么区别?
- 重载:同样的一个方法能够根据输入数据的不同,做出不同的处理
- 重写:当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,就要覆盖父类方法
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载
发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
重写
- 方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
- 如果父类方法访问修饰符为
private/final/static
则子类就不能重写该方法,但是被static
修饰的方法能够被再次声明。 - 构造方法无法被重写
方法的重写要遵循“两同两小一大”:
- “两同”即方法名相同、形参列表相同;
- “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
- “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。
关于 重写的返回值类型 这里需要额外多说明一下,上面的表述不太清晰准确:如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。
面向对象基础
封装
封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法(get/set)来操作属性。就好像我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。就好像如果没有空调遥控器,那么我们就无法操控空凋制冷,空调本身就没有意义了
继承
不同类型的对象,相互之间经常有一定数量的共同点。例如,小明同学、小红同学、小李同学,都共享学生的特性(班级、学号等)。同时,每一个对象还定义了额外的特性使得他们与众不同。例如小明的数学比较好,小红的性格惹人喜爱;小李的力气比较大。继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。
多态
多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
多态的特点:
- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
- 多态不能调用“只在子类存在但在父类不存在”的方法;
- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
Q1:抽象类是什么?
抽象类是一种特殊的类,它不能被实例化,只能被继承。抽象类用于定义一系列相关的类的通用行为和属性,它本身可以包含具体的方法实现,也可以包含抽象方法的声明。 定义抽象类的关键字是abstract
。抽象类可以具有抽象方法和非抽象方法
Q2:接口和抽象类有什么共同点和区别?
共同点:
- 都不能被实例化。
- 都可以包含抽象方法。
- 都可以有默认实现的方法(Java 8 可以用
default
关键字在接口中定义默认方法)。
区别:
-
抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
-
抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;
-
抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;
-
一个类只能继承一个抽象类,而一个类却可以实现多个接口。
Q3:深拷贝和浅拷贝的区别
浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
public class Address implements Cloneable{
private String name;
// 省略构造函数、Getter&Setter方法
@Override
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class Person implements Cloneable {
private Address address; //属性是引用类型
// 省略构造函数、Getter&Setter方法
@Override
public Person clone() {
try {
Person person = (Person) super.clone();
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
//测试
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// true
System.out.println(person1.getAddress() == person1Copy.getAddress());
深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
@Override
public Person clone() {
try {
Person person = (Person) super.clone();
person.setAddress(person.getAddress().clone());
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
//测试
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// false
System.out.println(person1.getAddress() == person1Copy.getAddress());
Object
Q1:== 和 equals() 的区别
==
对于基本类型和引用类型的作用效果是不同的:
- 对于基本数据类型来说,
==
比较的是值。 - 对于引用数据类型来说,
==
比较的是对象的内存地址。
equals()
方法存在两种使用情况:
- 类没有重写
equals()
方法:通过equals()
比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是Object
类equals()
方法。 - 类重写了
equals()
方法:一般我们都重写equals()
方法来比较两个对象中的内容是否相等;若相等,则返回 true(即,认为这两个对象相等)。
Q2:hashCode()有什么用?
hashCode()
的作用是获取哈希码(int
整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
Q3:为什么会有hashCode?
比如说hashSet如何检查重复。
当把对象加入到hashSet时,会计算对象的hashCode值判断加入的位置,同时也会与其他已经加入到hashSet的对象的值进行比较,如果没有一样的hashCode,hashSet会假设对象没有重复出现,如果发现有相同hashCode的对象,会调用equals()方法检查相同hashCode值的对象是否相等。如果相等hashSet就不会让其加入成功,如果不同就重新散列到其他位置。大大减少了equals的次数,提高执行速度。
Q4:为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的 hashCode
值必须是相等。也就是说如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
如果重写 equals()
时没有重写 hashCode()
方法的话就可能会导致 equals
方法判断是相等的两个对象,hashCode
值却不相等。
String
Q1:String、StringBuffer、StringBuilder 的区别?
- 可变性
String
是不可变的。对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新的值保存进去.**
StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder
类,在 AbstractStringBuilder
中也是使用字符数组保存字符串,不过没有使用 final
和 private
关键字修饰,最关键的是这个 AbstractStringBuilder
类还提供了很多修改字符串的方法比如 append
方法。
- 线程安全性
String
中的对象是不可变的,也就可以理解为常量,线程安全。
StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
- 性能
String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象。
StringBuffer
每次都会对它本身进行操作,而不是生成新的对象。
StringBuilder
相比使用 StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
- 操作少量的数据: 适用
String
- 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
Q2:String 为什么是不可变的?
- 保存字符串的数组被
final
修饰且为私有的,并且String
类没有提供/暴露修改这个字符串的方法。 String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
不可变。
如何理解 String 类型值的不可变? - 知乎提问
Q3:String str="aaa"与 String str=new String(“aaa”)一样吗?new String(“aaa”)创建了几个字符串对象?
- 一个或两个对象
- 使用
String a = “aaa” ;
,程序运行时会在常量池中查找”aaa”字符串,若没有,会将”aaa”字符串放进常量池,再将其地址赋给a;若有,将找到的”aaa”字符串的地址赋给a。 - 使用String b = new String(“aaa”);`,程序会在堆内存中开辟一片新空间存放新对象,同时会将”aaa”字符串放入常量池,相当于创建了两个对象,无论常量池中有没有”aaa”字符串,程序都会在堆内存中开辟一片新空间存放新对象。
Q4: 字符串拼接用“+” 还是 StringBuilder?
通过 StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象 。
Q5: String的equals() 和 Object的equals() 有何区别?
String
中的 equals
方法是被重写过的,比较的是 String
字符串的值是否相等。 Object
的 equals
方法是比较的对象的内存地址
Q6:字符串常量池的作用了解吗?
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
JDK1.6放在方法区,1.7以后放在堆。
Q7:JDK 1.7 为什么要将字符串常量池移动到堆中?
主要是因为方法区实现的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
Q8:String的intern方法有什么作用?
String.intern()
是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
Q9:String 类型的变量和常量做“+”运算时发生了什么?
final
关键字拼接的情况(JDK1.8):
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 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
对于 String str3 = "str" + "ing";
编译器会给你优化成 String str3 = "string";
。
并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:
- 基本数据类型(
byte
、boolean
、short
、char
、int
、float
、long
、double
)以及字符串常量。 final
修饰的基本数据类型和字符串变量- 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
引用的值在程序编译期是无法确定的,编译器无法对其进行优化。
final String str1 = "str";// final 修饰的字符串变量
final String str2 = "ing";// final 修饰的字符串变量
// 下面两个表达式其实是等价的
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true
被 final
关键字修饰之后的 String
会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。
异常
Q1:Exception 和 Error 有什么区别?
在 Java 中,所有的异常都有一个共同的祖先 java.lang
包中的 Throwable
类。Throwable
类有两个重要的子类:
Exception
:程序本身可以处理的异常,可以通过catch
来进行捕获。Exception
又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。Error
:Error
属于程序无法处理的错误 ,不建议通过catch
捕获 。例如 Java 虚拟机运行错误(Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止
Q2:Checked Exception 和 Unchecked Exception 有什么区别?
Checked Exception为受检异常,如果没有被catch或者throws关键字处理就没办法编译
- Checked Exception:
- IO异常
- SQL异常
- ClassNotFoundException
- Unchecked Exception:
NullPointerException
(空指针错误)IllegalArgumentException
(参数错误比如方法入参类型错误)NumberFormatException
(字符串转换为数字格式错误,IllegalArgumentException
的子类)ArrayIndexOutOfBoundsException
(数组越界错误)ClassCastException
(类型转换错误)ArithmeticException
(算术错误)SecurityException
(安全错误比如权限不够)
Q3:Throwable 类常用方法有哪些?
String getMessage()
: 返回异常发生时的简要描述String toString()
: 返回异常发生时的详细信息String getLocalizedMessage()
: 返回异常对象的本地化信息。使用Throwable
的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()
返回的结果相同void printStackTrace()
: 在控制台上打印Throwable
对象封装的异常信息
Q4:finally 中的代码一定会执行吗?
不一定的!在某些情况下,finally 中的代码不会被执行。
就比如说 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。另外,在以下 2 种特殊情况下,finally
块的代码也不会被执行:
- 程序所在的线程死亡。
- 关闭 CPU。
泛型
Q1:什么是泛型?有什么作用
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList<Person> persons = new ArrayList<Person>()
这行代码就指明了该 ArrayList
对象只能传入 Person
对象,如果传入其他类型的对象就会报错。
Q2:泛型的使用方式有哪几种?
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
- 泛型类:
//此处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);
- 泛型接口:
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";
}
}
3.泛型方法:
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
使用:
// 创建不同类型数组:Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray );
printArray( stringArray );
Q3:项目那里用到了泛型?
-
自定义接口通用返回结果
CommonResult<T>
通过参数T
可根据具体的返回类型动态指定结果的数据类型:在使用CommonResult<User>
类的时候,它的返回结果的数据类型就是User
。 -
定义
Excel
处理类ExcelUtil<T>
用于动态指定Excel
导出的数据类型 -
构建集合工具类(参考
Collections
中的sort
,binarySearch
方法)。
Q4:什么是泛型擦除,为什么擦除?
使用泛型的时候加上的类型参数,编译器在编译的时候去掉类型参数。
编译器会在编译期间动态的将泛型T擦除为Object,或将T extends xxx擦除为限定类型xxx
泛型为编译器的行为,为了保证引入泛型机制但不创建新的类型,减少虚拟机的运行开销,编译器通过类型擦除将泛型类转化为一般类。
Q5:什么是泛型中的限定通配符和非限定通配符 ?
限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界,另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。
非限定通配符 ?,可以用任意类型来替代。如List<?>
的意思是这个集合是一个可以持有任意类型的集合,它可以是List<A>
,也可以是List<B>
,或者List<C>
等等。
反射
Q1:什么是反射?
通过反射可以获取任意一个类的所有属性和方法,还可以调用这些方法和属性。
Q2:反射的应用场景了解么?
-
像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
-
Java 中的一大利器 注解 的实现也用到了反射。
为什么你使用 Spring 的时候 ,一个@Component
注解就声明了一个类为 Spring Bean 呢?为什么你通过一个@Value
注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?这些都是因为可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。获取到注解之后,就可以做进一步的处理 -
JDBC 的数据库的连接
在JDBC 的操作中,如果要想进行数据库的连接,则必须按照以上的几步完成
- 通过Class.forName()加载数据库的驱动程序 (通过反射加载,前提是引入相关了Jar包);
- 通过 DriverManager 类进行数据库的连接,连接的时候要输入数据库的连接地址、用户名、密码;
- 通过Connection 接口接收连接。
public class ConnectionJDBC {
/**
* @param args
*/
//驱动程序就是之前在classpath中配置的JDBC的驱动程序的JAR 包中
public static final String DBDRIVER = "com.mysql.jdbc.Driver";
//连接地址是由各个数据库生产商单独提供的,所以需要单独记住
public static final String DBURL = "jdbc:mysql://localhost:3306/test";
//连接数据库的用户名
public static final String DBUSER = "root";
//连接数据库的密码
public static final String DBPASS = "";
public static void main(String[] args) throws Exception {
Connection con = null; //表示数据库的连接对象
Class.forName(DBDRIVER); //1、使用CLASS 类加载驱动程序 ,反射机制的体现
con = DriverManager.getConnection(DBURL,DBUSER,DBPASS); //2、连接数据库
System.out.println(con);
con.close(); // 3、关闭数据库
}
Q3:反射的优缺点
优点:能够运行时动态获取类的实例,提高灵活性,为各种框架提供开箱即用的功能提供了便利。
缺点:使用反射性能较低,需要解析字节码,将内存中的对象进行解析。其解决方案是:通过setAccessible(true)关闭JDK的安全检查来提升反射速度;多次创建一个类的实例时,有缓存会快很多;也增加了安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)
Q4:获取Class对象的四种方式?
-
通过Class.forName(“类的路径”)
Class clz = Class.forName("java.lang.String");
-
类名.class
-
对象名.getClass()
Q5:反射常用的方法
- 获取类信息:
Class.forName(String className)
:通过类的全限定名获取对应的Class
对象。obj.getClass()
:通过对象实例获取其对应的Class
对象。ClassLoader.loadClass(String className)
:通过类加载器加载类,并获取对应的Class
对象。
- 获取字段信息:
Class.getDeclaredField(String fieldName)
:获取指定名称的字段对象(包括私有字段)。Class.getField(String fieldName)
:获取指定名称的公共字段对象。
- 获取方法信息:
Class.getDeclaredMethod(String methodName, Class<?>... parameterTypes)
:获取指定名称和参数类型的方法对象(包括私有方法)。Class.getMethod(String methodName, Class<?>... parameterTypes)
:获取指定名称和参数类型的公共方法对象。
- 获取构造方法信息:
Class.getDeclaredConstructor(Class<?>... parameterTypes)
:获取指定参数类型的构造方法对象(包括私有构造方法)。Class.getConstructor(Class<?>... parameterTypes)
:获取指定参数类型的公共构造方法对象。
- 操作字段:
Field.get(Object obj)
:获取指定对象中某个字段的值。Field.set(Object obj, Object value)
:设置指定对象中某个字段的值。
- 操作方法:
Method.invoke(Object obj, Object... args)
:调用指定对象的方法。Method.getReturnType()
:获取方法的返回类型。Method.getParameterTypes()
:获取方法的参数类型。
- 操作构造方法:
Constructor.newInstance(Object... initargs)
:使用指定的参数创建对象实例。
序列化
Q1:什么是序列化,什么是反序列?
- 序列化:将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
Q2:序列化和反序列化的应用场景
- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
Q3:如果有些字段不想进行序列化怎么办?
对于不想进行序列化的变量,使用 transient
关键字修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient
修饰的变量值不会被持久化和恢复。
Q4:常见序列化协议有哪些?
JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。
IO
Q1:Java IO流了解吗?
IO即Input/Output
,输入和输出。数据输入到计算机内存的过程为输入,反之输出到外部存储的过程为输出。数据传输过程为IO流。IO流在Java中分为输入流和输出流,而根据数据处理方式又分为字节流和字符流。
InputStream
/Reader
: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。OutputStream
/Writer
: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
Q2:字节流如何转为字符流?
字节输入流转字符输入流通过 InputStreamReader 实现,该类的构造函数可以传入 InputStream 对象。
字节输出流转字符输出流通过 OutputStreamWriter 实现,该类的构造函数可以传入 OutputStream 对象。
Q3:字符流与字节流的区别?
- 读写的时候字节流是按字节读写,字符流按字符读写。
- 字节流适合所有类型文件的数据传输,因为计算机字节(Byte)是电脑中表示信息含义的最小单位。字符流只能够处理纯文本数据,其他类型数据不行,但是字符流处理文本要比字节流处理文本要方便。
- 在读写文件需要对内容按行处理,比如比较特定字符,处理某一行数据的时候一般会选择字符流。
- 只是读写文件,和文件内容无关时,一般选择字节流。
Q4:什么是IO?
从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。
从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。
当应用程序发起 I/O 调用后,会经历两个步骤:
- 内核等待 I/O 设备准备好数据
- 内核将数据从内核空间拷贝到用户空间。
Q5:BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt"))
为什么这么写?
这段代码使用了装饰器模式,将一个 FileInputStream
对象包装在 BufferedInputStream
中,进一步提供了缓冲功能。 在 Java 中,文件的读取操作较慢,每次读取一个字节或一个字符都可能导致 IO 操作,从而产生较高的开销。为了提高文件读取的效率,可以使用缓冲流(BufferedInputStream
和 BufferedReader
等)来减少 IO 操作。 BufferedInputStream
是一个缓冲输入流,它继承自 FilterInputStream
。当我们使用 BufferedInputStream
对象读取文件时,它会先将文件的内容读取到内存缓冲区中,然后从缓冲区中读取数据,从而减少了实际的磁盘 I/O 操作次数。 在给定的代码中,首先创建了一个 FileInputStream
对象,用于读取文件 “input.txt” 的内容。然后,这个 FileInputStream
对象被传递给 BufferedInputStream
的构造方法中作为参数,创建了一个 BufferedInputStream
对象。这样,我们就得到了一个能够读取指定文件内容并具有缓冲功能的流对象。 通过使用 BufferedInputStream
,可以有效地提高文件的读取速度,尤其当读取大文件时,效果更为明显。
Q6:IO有哪些模型
- BIO(Blocking I/O)
BIO 属于同步阻塞 IO 模型 。
同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。
- NIO(Non-blocking/New I/O)
NIO为同步非阻塞,程序发起调用,等待数据从内核拷贝到用户空间的这段时间,线程依然是阻塞的,知道内核把数据拷贝到用户空间,相比于BIO,NIO有很大的改进,通过轮询避免一直阻塞。但是,应用程序不断地进行IO调用轮询数据是否已经准备好的过程是十分消耗CPU资源的。
IO多路复用模型,线程首先发起select调用,询问内核数据是否准备好,等内核把数据准备好了,用户线程再发起read调用。read调用的过程中还是阻塞的。但是IO多路复用模型通过减少无效的系统调用,减少了对CPU资源的消耗。
异步 IO ,异步并非阻塞。也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
Q7:Java IO都有哪些设计模式?
- 装饰器模式
装饰器(Decorator)模式 可以在不改变原有对象的情况下拓展其功能。
装饰器模式通过组合替代继承来扩展原始类的功能,在一些继承关系比较复杂的场景(IO 这一场景各种类的继承关系就比较复杂)更加实用。
对于字节流来说, FilterInputStream
(对应输入流)和FilterOutputStream
(对应输出流)是装饰器模式的核心,分别用于增强 InputStream
和OutputStream
子类对象的功能。
我们常见的BufferedInputStream
(字节缓冲输入流)、DataInputStream
等等都是FilterInputStream
的子类,BufferedOutputStream
(字节缓冲输出流)、DataOutputStream
等等都是FilterOutputStream
的子类。
- 适配器模式
适配器(Adapter Pattern)模式 主要用于接口互不兼容的类的协调工作,你可以将其联想到我们日常经常使用的电源适配器。
适配器模式中存在被适配的对象或者类称为 适配者(Adaptee) ,作用于适配者的对象或者类称为适配器(Adapter) 。适配器分为对象适配器和类适配器。类适配器使用继承关系来实现,对象适配器使用组合关系来实现。
IO 流中的字符流和字节流的接口不同,它们之间可以协调工作就是基于适配器模式来做的,更准确点来说是对象适配器。通过适配器,我们可以将字节流对象适配成一个字符流对象,这样我们可以直接通过字节流对象来读取或者写入字符数据。
InputStreamReader
和 OutputStreamWriter
就是两个适配器(Adapter), 同时,它们两个也是字节流和字符流之间的桥梁。InputStreamReader
使用 StreamDecoder
(流解码器)对字节进行解码,实现字节流到字符流的转换, OutputStreamWriter
使用StreamEncoder
(流编码器)对字符进行编码,实现字符流到字节流的转换。
InputStream
和 OutputStream
的子类是被适配者, InputStreamReader
和 OutputStreamWriter
是适配器。