深度干货 | 38道Java基础面试题 (1.2W字详细解析)
Posted 林小鹿@
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度干货 | 38道Java基础面试题 (1.2W字详细解析)相关的知识,希望对你有一定的参考价值。
目录
基础语法
1、Java 语言的优点?
1、平台无关性,摆脱硬件束缚,“一次编写,到处运行”。
2、相对安全的内存管理和访问机制,避免大部分内存泄漏和指针越界。
3、热点代码检测和运行时编译及优化,使程序随运行时间增长获得更高性能。
4、 完善的应用程序接口,支持第三方类库。
5、持网络编程并且很方便。
2、Java 如何实现平台无关?
JVM: Java 编译器可生成与计算机体系结构无关的字节码指令,字节码文件不仅可以轻易地在任何机器上解释执行,还可以动态地转换成本地机器代码,转换是由 JVM 实现的,JVM 是平台相关的,屏蔽了不同操作系统的差异。
语言规范: 基本数据类型大小有明确规定,例如 int 永远为 32 位,而 C/C++ 中可能是 16 位、32 位,也可能是编译器开发商指定的其他大小。Java 中数值类型有固定字节数,二进制数据以固定格式存储和传输,字符串采用标准的 Unicode 格式存储。
3、JVM,JDK 和 JRE 的区别?
JDK: Java Development Kit,开发工具包。提供了编译运行 Java 程序的各种工具,包括编译器、JRE 及常用类库,是 JAVA 核心。
JRE: Java Runtime Environment,运行时环境,运行 Java 程序的必要环境,包括 JVM、核心类库、核心配置工具。
JDK包含JRE,JRE包含JVM。
4、Java 按值调用还是引用调用?
按值调用指方法接收调用者提供的值,按引用调用指方法接收调用者提供的变量地址。
Java 总是按值调用,方法得到的是所有参数值的副本,传递对象时实际上方法接收的是对象引用的副本。方法不能修改基本数据类型的参数,如果传递了一个 int 值 ,改变值不会影响实参,因为改变的是值的一个副本。
可以改变对象参数的状态,但不能让对象参数引用一个新的对象。如果传递了一个 int 数组,改变数组的内容会影响实参,而改变这个参数的引用并不会让实参引用新的数组对象。
5、浅拷贝和深拷贝的区别?
浅拷贝: 只复制当前对象的基本数据类型及引用变量,没有复制引用变量指向的实际对象。修改克隆对象可能影响原对象,不安全。
深拷贝: 完全拷贝基本数据类型和引用数据类型,安全。
6、什么是反射?
概念: 在运行状态中,对于任意一个类都能知道它的所有属性和方法,对于任意一个对象都能调用它的任意方法和属性,这种动态获取信息及调用对象方法的功能称为反射。
原理: 通过将类对应的字节码文件加载到jvm内存中得到一个Class对象,通过这个Class对象可以反向获取实例的各个属性以及调用它的方法。
使用场景:
1、通过反射运行配置文件内容
- 加载配置文件,并解析配置文件得到相应信息
- 根据解析的字符串利用反射机制获取某个类的Class实例动态配置属性
2、JDK动态代理
3、jdbc通过Class.forName()加载数据的驱动程序
4、Spring解析xml装配Bean
7、Class 类的作用?如何获取一个 Class 对象?
在程序运行期间,Java 运行时系统为所有对象维护一个运行时类型标识,这个信息会跟踪每个对象所属的类,虚拟机利用运行时类型信息选择要执行的正确方法,保存这些信息的类就是 Class,这是一个泛型类。
获取 Class 对象:① 类名.class
。②对象的 getClass
方法。③ Class.forName(类的全限定名)
。
8、什么是注解?什么是元注解?
注解是一种标记,使类或接口附加额外信息,帮助编译器和 JVM 完成一些特定功能,例如 @Override
标识一个方法是重写方法。
元注解是自定义注解的注解,例如:
@Target
:约束作用位置,值是 ElementType 枚举常量,包括 METHOD 方法、VARIABLE 变量、TYPE 类/接口、PARAMETER 方法参数、CONSTRUCTORS 构造方法和 LOACL_VARIABLE 局部变量等。
@Rentention
:约束生命周期,值是 RetentionPolicy 枚举常量,包括 SOURCE 源码、CLASS 字节码和 RUNTIME 运行时。
@Documented
:表明这个注解应该被 javadoc 记录。
9、什么是泛型,有什么作用?
泛型就是将类型参数化,其在编译时才确定具体的参数
泛型的好处:
1、 类型安全,放置什么出来就是什么,不存在 ClassCastException。
2、 提升可读性,编码阶段就显式知道泛型集合、泛型方法等处理的对象类型。
3、 代码重用,合并了同类型的处理代码。
4、消除强制类型转换
10、泛型擦除是什么?
泛型擦除:使用泛型的时候加上的类型参数,编译器在编译的时候去掉类型参数。
泛型用于编译阶段,编译后的字节码文件不包含泛型类型信息,因为虚拟机没有泛型类型对象,所有对象都属于普通类。例如定义 List<Object>
或 List<String>
,在编译后都会变成 List
。
定义一个泛型类型,会自动提供一个对应原始类型,类型变量会被擦除。如果没有限定类型就会替换为 Object,如果有限定类型就会替换为第一个限定类型,例如 <T extends A & B>
会使用 A 类型替换 T。
11、JDK8 新特性有哪些?
lambda 表达式: 允许把函数作为参数传递到方法,简化匿名内部类代码。
函数式接口: 使用 @FunctionalInterface
标识,有且仅有一个抽象方法,可被隐式转换为 lambda 表达式。
方法引用: 可以引用已有类或对象的方法和构造方法,进一步简化 lambda 表达式。
接口: 接口可以定义 default
修饰的默认方法,降低了接口升级的复杂性,还可以定义静态方法。
注解: 引入重复注解机制,相同注解在同地方可以声明多次。注解作用范围也进行了扩展,可作用于局部变量、泛型、方法异常等。
类型推测: 加强了类型推测机制,使代码更加简洁。
Optional 类: 处理空指针异常,提高代码可读性。
Stream 类: 引入函数式编程风格,提供了很多功能,使代码更加简洁。方法包括 forEach
遍历、count
统计个数、filter
按条件过滤、limit
取前 n 个元素、skip
跳过前 n 个元素、map
映射加工、concat
合并 stream 流等。
日期: 增强了日期和时间 API,新的 java.time 包主要包含了处理日期、时间、日期/时间、时区、时刻和时钟等操作。
JavaScript: 提供了一个新的 javascript 引擎,允许在 JVM上运行特定 JavaScript 应用。
12、异常有哪些分类?
所有异常都是 Throwable 的子类,分为 Error 和 Exception。Exception的子类为RuntimeException异常和RuntimeException以外的异常(例如IOException)。
因此异常主要分为Error,RuntimeException异常和RuntimeException以外的异常。(错误、运行时异常和编译时异常)
Error就是一些程序处理不了的错误,代表JVM出现了一些错误,应用程序无法处理。例如当JVM不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError
常见异常: NullPointerException、ClassNotFoundException、arrayindexoutofboundsexception、ClassCastException(类型强制转换)
数据类型
1、Java 有哪些基本数据类型?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CMxcCBDr-1655393678043)(Java基础.assets/image-20210219172725756.png)]
数据类型 | 内存大小 | 默认值 | 取值范围 |
---|---|---|---|
byte | 1 B | (byte)0 | -128 ~ 127 |
short | 2 B | (short)0 | -215 ~ 215-1 |
int | 4 B | 0 | -231 ~ 231-1 |
long | 8 B | 0L | -263 ~ 263-1 |
float | 4 B | 0.0F | ±3.4E+38(有效位数 6~7 位) |
double | 8 B | 0.0D | ±1.7E+308(有效位数 15 位) |
char | 英文 1B,中文 UTF-8 占 3B,GBK 占 2B。 | ‘\\u0000’ | ‘\\u0000’ ~ ‘\\uFFFF’ |
boolean | 单个变量 4B / 数组 1B | false | true、false |
JVM 没有 boolean 赋值的专用字节码指令,boolean f = false
就是使用 ICONST_0 即常数 0 赋值。单个 boolean 变量用 int 代替,boolean 数组会编码成 byte 数组。
2、自动装箱/拆箱是什么?
每个基本数据类型都对应一个包装类,除了 int 和 char 对应 Integer 和 Character 外,其余基本数据类型的包装类都是首字母大写即可。
自动装箱: 将基本数据类型包装为一个包装类对象,例如向一个泛型为 Integer 的集合添加 int 元素。
自动拆箱: 将一个包装类对象转换为一个基本数据类型,例如将一个包装类对象赋值给一个基本数据类型的变量。
比较两个包装类数值要用 equals
,而不能用 ==
。
3、String 是不可变类为什么值可以修改?
String 类和其存储数据的成员变量 value 字节数组都是 final 修饰的。对一个 String 对象的任何修改实际上都是创建一个新 String 对象,再引用该对象。只是修改 String 变量引用的对象,没有修改原 String 对象的内容。
4、字符串拼接的方式有哪些?
① 直接用 +
,底层用 StringBuilder 实现。只适用小数量,如果在循环中使用 +
拼接,相当于不断创建新的 StringBuilder 对象再转换成 String 对象,效率极差。
② 使用 String 的 concat 方法,该方法中使用 Arrays.copyOf
创建一个新的字符数组 buf 并将当前字符串 value 数组的值拷贝到 buf 中,buf 长度 = 当前字符串长度 + 拼接字符串长度。之后调用 getChars
方法使用 System.arraycopy
将拼接字符串的值也拷贝到 buf 数组,最后用 buf 作为构造参数 new 一个新的 String 对象返回。效率稍高于直接使用 +
。
③ 使用 StringBuilder 或 StringBuffer,两者的 append
方法都继承自 AbstractStringBuilder,该方法首先使用 Arrays.copyOf
确定新的字符数组容量,再调用 getChars
方法使用 System.arraycopy
将新的值追加到数组中。StringBuilder 是 JDK5 引入的,效率高但线程不安全。StringBuffer 使用 synchronized 保证线程安全。
5、String a = “a” + new String(“b”) 创建了几个对象?
常量和常量拼接仍是常量,结果在常量池,只要有变量参与拼接结果就是变量,存在堆。
使用字面量时只创建一个常量池中的常量,使用 new 时如果常量池中没有该值就会在常量池中新创建,再在堆中创建一个对象引用常量池中常量。因此 String a = "a" + new String("b")
会创建四个对象,常量池中的 a 和 b,堆中的 b 和堆中的 ab。
面向对象
1、谈一谈你对面向对象的理解 ?
面向过程: 一件事该怎么做,注重实现过程,以过程为中心
面向对象: 实现对象是谁,只关心怎样使用,不关心具体实现(只关心实现对象是谁,有封装、继承、多态三大特性)
面向对象是一种编程思想,早期的面向过程的思想就是一件事该怎么做,而面向对象就是一件事该由谁来做,它怎么做的我不管,我只需要调用就行。而这些是由面向对象的三大特性来实现的,三大特性就是封装、继承、多态。封装就是将一类属性和行为抽象成一个类,
使其属性私有化,行为公开化,提高属性的安全性的同时,也可以使代码模块化,这样做使代码的复用性更高。继承就是将几个类共有的属性和行为抽象成一个父类,每个子类都有父类的属性和行为,也有自己的属性和行为,这样做,扩展了已存在的代码,进一步提高的代码的复用性,但是继承是耦合度很高的一种关系,父类代码修改,子类行为也会改变,如果过度使用继承会起到反效果。多态必须要有继承和重写,并且父类/接口引用指向子类/实现类对象,很多的设计模式都是基于面向对象中多态性设计的。
2、面向对象的三大特性?
**封装: ** 可以不用关心内部实现,具体构造,只需知道怎么操作它就是,比如电视,手机,将内部封装起来,直接使用
多态: 同一个方法调用,由于对象不同可能会有不同的行为。比如都是休息,张三是睡觉,李四是爬山等;或则具体场景中,我不知道现在具体传进来的对象是student,还是teacher,那么可以用pepole去接收它。
多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象。3.父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了
继承: 使代码更容易扩展,比如有学生教师,他们都有一些公用方法与属性,可以将其抽取出来定义为父类,再去继承它,复用代码,减少冗余,易于扩展
3、重载和重写的区别?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载: 发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分
重写: 发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,
抛出的异常
小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。
4、类之间有哪些关系?
类关系 | 描述 | 权力强侧 | 举例 |
---|---|---|---|
继承 | 父子类之间的关系:is-a | 父类 | 小狗继承于动物 |
实现 | 接口和实现类之间的关系:can-do | 接口 | 小狗实现了狗叫接口 |
组合 | 比聚合更强的关系:contains-a | 整体 | 头是身体的一部分 |
聚合 | 暂时组装的关系:has-a | 组装方 | 小狗和绳子是暂时的聚合关系 |
依赖 | 一个类用到另一个:depends-a | 被依赖方 | 人养小狗,人依赖于小狗 |
关联 | 平等的使用关系:links-a | 平等 | 人使用卡消费,卡可以提取人的信息 |
5、Object 类有哪些方法?
1、getClass: final方法,获得运行时类型。
2、toString: 对象的字符串表示形式(对象所属类的名称+@+转换为十六进制的对象的哈希值组成的字符串)
3、equas方法: 检测对象是否相等,默认使用 ==
比较对象引用,可以重写 equals 方法自定义比较规则。
4、Clone方法: 保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。
5、notify方法: 唤醒在该对象上等待的某个线程
6、notifyAll方法: 唤醒在该对象上等待的所有线程
7、wait方法: wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁,直到其他线程调用此对象的notify()方法或 notifyAll()方法”,当前线程被唤醒(进入“就绪状态”)。还有一个wait(long timeout)超时时间-补充sleep不会释放锁
8、Finalize()方法: 可以用于对象的自我拯救
9、Hashcode方法: 该方法用于哈希查找,可以减少在查找中使用equals的次数,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。一般必须满足obj1.equals(obj2) == true。可以推出obj1.hashCode() == obj2.hashCode(),但是hashCode相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价
6、内部类的作用是什么,有哪些分类?
内部类可对同一包中其他类隐藏,内部类方法可以访问定义这个内部类的作用域中的数据,包括 private 数据。
内部类是一个编译器现象,与虚拟机无关。编译器会把内部类转换成常规的类文件,用 $ 分隔外部类名与内部类名,其中匿名内部类使用数字编号,虚拟机对此一无所知。
静态内部类: 属于外部类,只加载一次。作用域仅在包内,可通过 外部类名.内部类名
直接访问,类内只能访问外部类所有静态属性和方法。HashMap 的 Node 节点,ReentrantLock 中的 Sync 类,ArrayList 的 SubList 都是静态内部类。内部类中还可以定义内部类,如 ThreadLoacl 静态内部类 ThreadLoaclMap 中定义了内部类 Entry。
成员内部类: 属于外部类的每个对象,随对象一起加载。不可以定义静态成员和方法,可访问外部类的所有内容。
局部内部类: 定义在方法内,不能声明访问修饰符,只能定义实例成员变量和实例方法,作用范围仅在声明类的代码块中。
匿名内部类: 只用一次的没有名字的类,可以简化代码,创建的对象类型相当于 new 的类的子类类型。用于实现事件监听和其他回调。
7、访问权限控制符有哪些?
访问权限控制符 | 本类 | 包内 | 包外子类 | 任何地方 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default | √ | √ | × | × |
private | √ | × | × | × |
8、接口和抽象类的异同?
相同: 接口和抽象类对实体类进行更高层次的抽象,仅定义公共行为和特征。
区别 | 抽象类 | 接口 |
---|---|---|
成员变量 | 无特殊要求,可以和普通类─样定义任意类型 | 只能是 public static final 常量 |
构造方法 | 有构造方法,不能实例化 | 没有构造方法,不能实例化 |
方法 | 抽象类可以没有抽象方法,但有抽象方法一定是抽象类。 | 接口只有定义,不能有方法的实现,JDK8 支持默认/静态方法,JDK9 支持私有方法 |
继承 | 单继承 | 多继承 |
9、接口和抽象类应该怎么选择?
抽象类体现 is-a 关系,接口体现 can-do 关系。与接口相比,抽象类通常是对同类事物相对具体的抽象。
抽象类是模板式设计,包含一组具体特征,例如某汽车,底盘、控制电路等是抽象出来的共同特征,但内饰、显示屏、座椅材质可以根据不同级别配置存在不同实现。
接口是契约式设计,是开放的,定义了方法名、参数、返回值、抛出的异常类型,谁都可以实现它,但必须遵守接口的约定。例如所有车辆都必须实现刹车这种强制规范。
接口是顶级类,抽象类在接口下面的第二层,对接口进行了组合,然后实现部分接口。当纠结定义接口和抽象类时,推荐定义为接口,遵循接口隔离原则,按维度划分成多个接口,再利用抽象类去实现这些,方便后续的扩展和重构。
例如 Plane 和 Bird 都有 fly 方法,应把 fly 定义为接口,而不是抽象类的抽象方法再继承,因为除了 fly 行为外 Plane 和 Bird 间很难再找到其他共同特征。
10、子类初始化的顺序
① 父类静态代码块和静态变量。② 子类静态代码块和静态变量。③ 父类普通代码块和普通变量。④ 父类构造方法。⑤ 子类普通代码块和普通变量。⑥ 子类构造方法。
String相关
1、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”字符串,程序都会在堆内存中开辟一片新空间存放新对象。
2、String有哪些特性?
- 不变性:String 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性;
- 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用;
- final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。
3、在使用 HashMap 的时候,用 String 做 key 有什么好处?
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
对象相等判断
1、== 和 equals 区别是什么?
如果==
比较的是基本数据类型,那么比较的是两个基本数据类型的值是否相等;
如果==
是比较的两个对象,那么比较的是两个对象的引用
equals方法主要用于两个对象之间,检测一个对象是否等于另一个对象
2、hashCode(),equals()两种方法是什么关系?
如果两个对象相等,则hashcode一定也是相同的;
两个对象相等,对两个对象分别调用equals方法都返回true;
两个对象有相同的hashcode值,它们也不一定是相等的;
3、为什么重写 equals 方法必须重写 hashcode 方法 ?
判断的时候先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。如果只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果为true。
在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到不同的位置而造成对象的不能覆盖的问题。[String,StringBuffer, StringBuilder 的区别是什么?
4、String,StringBuffer, StringBuilder 的区别是什么?
可变和不可变:
- string对象是不可变的
- StringBuilder与StringBuffer这两种对象都是可变的
是否多线程安全:
- String中的对象是不可变的,也就可以理解为常量,显然线程安全。
- StringBuilder是非线程安全的。
- StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
性能:
- 每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象,效率低。
- StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。
5、String为什么要设计成不可变的?
1、便于实现字符串池
-
在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。
-
如果字符串是可变的,某一个字符串变量改变了其值,那么其指向的变量的值也会改变,String pool将不能够实现!
2、使多线程安全
- 在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。
3、避免安全问题
- 在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。
4、加快字符串处理速度
- 由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。
IO 流
1、同步/异步/阻塞/非阻塞 IO 的区别?
同步和异步是通信机制,阻塞和非阻塞是调用状态。
同步 IO 是用户线程发起 IO 请求后需要等待或轮询内核 IO 操作完成后才能继续执行。异步 IO 是用户线程发起 IO 请求后可以继续执行,当内核 IO 操作完成后会通知用户线程,或调用用户线程注册的回调函数。
阻塞 IO 是 IO 操作需要彻底完成后才能返回用户空间 。非阻塞 IO 是 IO 操作调用后立即返回一个状态值,无需等 IO 操作彻底完成。
2、什么是 BIO?
BIO 是同步阻塞式 IO,JDK1.4 之前的 IO 模型。服务器实现模式为一个连接请求对应一个线程,服务器需要为每一个客户端请求创建一个线程,如果这个连接不做任何事会造成不必要的线程开销。可以通过线程池改善,这种 IO 称为伪异步 IO。适用连接数目少且服务器资源多的场景。
3、什么是 NIO?
NIO 是 JDK1.4 引入的同步非阻塞 IO。服务器实现模式为多个连接请求对应一个线程,客户端连接请求会注册到一个多路复用器 Selector ,Selector 轮询到连接有 IO 请求时才启动一个线程处理。适用连接数目多且连接时间短的场景。
同步是指线程还是要不断接收客户端连接并处理数据,非阻塞是指如果一个管道没有数据,不需要等待,可以轮询下一个管道。
核心组件:
-
Selector: 多路复用器,轮询检查多个 Channel 的状态,判断注册事件是否发生,即判断 Channel 是否处于可读或可写状态。使用前需要将 Channel 注册到 Selector,注册后会得到一个 SelectionKey,通过 SelectionKey 获取 Channel 和 Selector 相关信息。
-
Channel: 双向通道,替换了 BIO 中的 Stream 流,不能直接访问数据,要通过 Buffer 来读写数据,也可以和其他 Channel 交互。
-
Buffer: 缓冲区,本质是一块可读写数据的内存,用来简化数据读写。Buffer 三个重要属性:position 下次读写数据的位置,limit 本次读写的极限位置,capacity 最大容量。
flip
将写转为读,底层实现原理把 position 置 0,并把 limit 设为当前的 position 值。clear
将读转为写模式(用于读完全部数据的情况,把 position 置 0,limit 设为 capacity)。compact
将读转为写模式(用于存在未读数据的情况,让 position 指向未读数据的下一个)。- 通道方向和 Buffer 方向相反,读数据相当于向 Buffer 写,写数据相当于从 Buffer 读。
使用步骤:向 Buffer 写数据,调用 flip 方法转为读模式,从 Buffer 中读数据,调用 clear 或 compact 方法清空缓冲区。
4、什么是 AIO?
AIO 是 JDK7 引入的异步非阻塞 IO。服务器实现模式为一个有效请求对应一个线程,客户端的 IO 请求都是由操作系统先完成 IO 操作后再通知服务器应用来直接使用准备好的数据。适用连接数目多且连接时间长的场景。
异步是指服务端线程接收到客户端管道后就交给底层处理IO通信,自己可以做其他事情,非阻塞是指客户端有数据才会处理,处理好再通知服务器。
实现方式包括通过 Future 的 get
方法进行阻塞式调用以及实现 CompletionHandler 接口,重写请求成功的回调方法 completed
和请求失败回调方法 failed
。
5、java.io 包下有哪些流?
主要分为字符流和字节流,字符流一般用于文本文件,字节流一般用于图像或其他文件。
字符流包括了字符输入流 Reader 和字符输出流 Writer,字节流包括了字节输入流 InputStream 和字节输出流 OutputStream。字符流和字节流都有对应的缓冲流,字节流也可以包装为字符流,缓冲流带有一个 8KB 的缓冲数组,可以提高流的读写效率。除了缓冲流外还有过滤流 FilterReader、字符数组流 CharArrayReader、字节数组流 ByteArrayInputStream、文件流 FileInputStream 等。
6、序列化和反序列化是什么?
Java 对象 JVM 退出时会全部销毁,如果需要将对象及状态持久化,就要通过序列化实现,将内存中的对象保存在二进制流中,需要时再将二进制流反序列化为对象。对象序列化保存的是对象的状态,因此属于类属性的静态变量不会被序列化。
常见的序列化有三种:
-
Java 原生序列化
实现
Serializabale
标记接口,Java 序列化保留了对象类的元数据(如类、成员变量、继承类信息)以及对象数据,兼容性最好,但不支持跨语言,性能一般。序列化和反序列化必须保持序列化 ID 的一致,一般使用private static final long serialVersionUID
定义序列化 ID,如果不设置编译器会根据类的内部实现自动生成该值。如果是兼容升级不应该修改序列化 ID,防止出错,如果是不兼容升级则需要修改。 -
Hessian 序列化
Hessian 序列化是一种支持动态类型、跨语言、基于对象传输的网络协议。Java 对象序列化的二进制流可以被其它语言反序列化。Hessian 协议的特性:① 自描述序列化类型,不依赖外部描述文件,用一个字节表示常用基础类型,极大缩短二进制流。② 语言无关,支持脚本语言。③ 协议简单,比 Java 原生序列化高效。Hessian 会把复杂对象所有属性存储在一个 Map 中序列化,当父类和子类存在同名成员变量时会先序列化子类再序列化父类,因此子类值会被父类覆盖。
-
JSON 序列化
JSON 序列化就是将数据对象转换为 JSON 字符串,在序列化过程中抛弃了类型信息,所以反序列化时只有提供类型信息才能准确进行。相比前两种方式可读性更好,方便调试。
序列化通常会使用网络传输对象,而对象中往往有敏感数据,容易遭受攻击,Jackson 和 fastjson 等都出现过反序列化漏洞,因此不需要进行序列化的敏感属性传输时应加上 transient 关键字。transient 的作用就是把变量生命周期仅限于内存而不会写到磁盘里持久化,变量会被设为对应数据类型的零值。
以上是关于深度干货 | 38道Java基础面试题 (1.2W字详细解析)的主要内容,如果未能解决你的问题,请参考以下文章
深度干货 | 32道JVM基础面试题 (1.2W字详细解析)
深度干货 | 32道JVM基础面试题 (1.2W字详细解析)