| 序号 | 内容 | 链接地址 |
|---|---|---|
| 1 | Java基础八股文 | https://zhuanlan.zhihu.com/p/379234291 https://blog.youkuaiyun.com/weixin_39290259/article/details/117746292 |
| 2 | Java容器八股文 | 敬请期待 |
| 3 | Java异常八股文 | https://zhuanlan.zhihu.com/p/377557779 https://blog.youkuaiyun.com/weixin_39290259/article/details/117229021 |
| 4 | Java并发编程八股文 | 敬请期待 |
| 5 | JVM八股文 | 敬请期待 |
| 6 | Spring八股文 | 敬请期待 |
| 7 | Spring MVC八股文 | 敬请期待 |
| 8 | Spring Boot八股文 | 敬请期待 |
| 9 | MySQL八股文 | 敬请期待 |
| 10 | Redis八股文 | 敬请期待 |
| 11 | MyBatis八股文 | 敬请期待 |
| 12 | Netty八股文 | 敬请期待 |
| 13 | 网络原理八股文 | 敬请期待 |
| 14 | Linux八股文 | 敬请期待 |
| 15 | Tomcat八股文 | 敬请期待 |
| 16 | Spring Cloud八股文 | 敬请期待 |
| 17 | MQ与RabbitMQ八股文 | 敬请期待 |
| 18 | Dubbo八股文 | 敬请期待 |
| 19 | ZooKeeper八股文 | 敬请期待 |
| 20 | 设计模式八股文 | 敬请期待 |
文章目录
一、Java概述
1.1 JVM、JDK、JRE都是什么
JVM:Java Virtual Machine是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有不同的Java虚拟机,因此Java可以实现跨平台。
JRE:Java Runtime Environment 包括Java虚拟机和Java程序所需要的核心类库。核心类库主要是java.lang包,主要包含了基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包。如果只想要运行一个开发好的Java程序,只需要安装JRE即可。
JDK:Java Development Kit是提供给Java开发人员使用的,其中包含了JRE和开发工具,所以安装了JDK就不用再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)等。
Java程序从源码到运行的一般步骤:

JVM、JRE、JDK的关系图:

1.2 Java跨平台性的原理是什么
跨平台性是指,Java开发的程序经过一次编译后可以才多个系统平台上运行。
实现原理:不同的平台上安装对应的Java虚拟机,Java程序跑在虚拟机上即可。
1.3 Java语言的特点
- 简单易学(语法和c++相似)
- 面向对象(封装、继承、多态)
- 平台无关性(JVM虚拟机屏蔽了不同平台)
- 支持网络编程并且很方便
- 支持多线程
- 健壮性(强类型机制、异常处理、垃圾自动收集)
- 安全性
1.4 什么是字节码
字节码:Java源码经过编译器编译后形成的.class文件,不面向任何处理器,只面向Java虚拟机。
1.5 Java和C++的区别
- 都是面向对象的语言
- Java不提供指针来直接访问内存
- Java类不支持多继承
- Java有自动内存管理机制,而C++需要程序员手动释放内存
二、基础语法
2.1 数据类型
2.1.1 Java有哪些数据类型

2.1.2 switch能作用在哪些类型上
byte、short、int、long、char、String、enum可以,float、double、boolean不可以。
2.1.3 byte a = (byte) 128 a是几
a=-128,因为byte最大能表示127,byte是8个二进制位,最高位表示符号位,所以byte能表示的最大的数就是2的7次方-1=127,二进制表示就是01111111,01111111加1就是10000000,10000000就是0,而0已经被00000000代表了,所以规定10000000就代表-128。
2.1.4 float f=3.4 是否正确
不正确。3.4是双精度数,双精度(double)赋值给单精度数(float)会损失精度,所以需要强转float f = (float) 3.4或float f = 3.4f。
2.2 访问修饰符
2.2.1 Java中有哪些访问修饰符,有何区别
- private:在同一类内可见。使用对象:变量,方法。
- default(缺省,什么都不写):在同一包内可见。
- protected:对同一包内的类和所有子类可见。
- public:对所有类可见。
| 修饰符 | 当前类 | 同包 | 子类 | 其他包 |
|---|---|---|---|---|
| private | √ | × | × | × |
| default | √ | √ | × | × |
| protected | √ | √ | √ | × |
| public | √ | √ | √ | √ |
2.3 运算符
2.3.1 &和&&的区别
- &可以用来按位与和逻辑与
- &&短路与,&&左边的如果为false,后边的不进行运算
2.4 关键字
2.4.1 final的作用
用于修饰类、属性和方法。
- 被final修饰的类不可被继承
- 被final修饰的方法不可被重写
- 被final修饰的变量不可被改变,是引用不可变,引用指向的内容是可以被改变的
2.4.2 final finally finalize区别
- final用于修饰类、属性和方法。修饰的类不能被继承,修饰的方法不能被重写,修饰的属性的引用不能指向其他的值。
- finally用于try-catch-finally中,表示不管是否出现异常都会执行finally块,一般存放关闭资源的代码。
- finalize是Object的一个方法,当垃圾回收器将要回收对象所占内存之前被JVM调用。
2.4.3 this关键字的用法
this是指向对象本身的一个指针。用法大致分为三种:
- 普通的直接引用,this相当于是指向当前对象本身。
- 形参与成员名字重名,用this来区分。
- 构造函数内引用本类的构造函数。
2.4.4 super关键字的用法
- 普通的直接引用,super相当于是指向当前对象父类的引用,这样就可以使用super.xxx来引用父类成员。
- 调用父类的构造函数。
2.4.5 this与super的区别
- super引用当前对象的直接父类成员,this代表当前对象的引用。
- super()在子类中调用父类的构造方法,this()在本类中调用其他构造方法。
- this()和super()均需放在构造方法的第一行。
- this()和super()只能在构造方法中调用,且只能调用一次。
- this()和super()不能同时出现在同一个构造方法里。
- this和super都不能在static环境中使用。
2.4.6 static存在的主要意义
- static的主要意义就在于创建独立于具体对象的域变量和方法,从而没有对象就可以使用类的属性和方法。
- 用static静态代码块来优化程序性能。static块可以置于程序的任何地方,可以有多个静态代码块。在类初次被加载的时候,会按照static块的顺序执行静态代码块,并且只会执行一次。
2.4.7 static注意事项
- static的只能访问static的。
- 非static可以访问static的。
2.4.8 在⼀个静态⽅法内调⽤⼀个⾮静态成员为什么是⾮法的?
非静态成员需要通过对象来进行访问,而静态方法不可以通过具体对象进行调用。
2.4.9 类中各个部分的加载顺序
class Print {
public Print(String s){
System.out.print(s + " ");
}
}
public class Parent{
public static Print obj1 = new Print("1");
public Print obj2 = new Print("2");
public static Print obj3 = new Print("3");
static{
new Print("4");
}
public static Print obj4 = new Print("5");
public Print obj5 = new Print("6");
public Parent(){
new Print("7");
}
}
public class Child extends Parent{
static{
new Print("a");
}
public static Print obj1 = new Print("b");
public Print obj2 = new Print("c");
public Child (){
new Print("d");
}
public static Print obj3 = new Print("e");
public Print obj4 = new Print("f");
public static void main(String [] args){
Parent obj1 = new Child ();
}
}
Child类继承自Parent类,输出结果:
1 3 4 5 a b e 2 6 7 c f d
所以加载顺序是:
- 加载父类中的静态的,包括静态变量的初始化和静态代码块的,静态变量和静态代码块的执行顺序是按照代码的编写顺序。
- 加载子类中的静态的。
- 执行父类中非静态变量的初始化。
- 执行父类的构造方法。
- 执行子类中非静态变量的初始化。
- 执行子类的构造方法。
三、面向对象
3.1 面向对象概述
3.1.1 面向对象和面向过程的区别
- 面向过程:就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。性能比面向对象高,因为面向对象的类调用时需要实例化,开销大。单片机、嵌入式开发一般采用面向过程开发。缺点就是开发出来的代码不易维护、不易复用、不易扩展。
- 面向对象:是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。易维护、易复用、易扩展。由于面向对象有封装、继承、多态的特性,可以设计出高内聚低耦合的系统。缺点就是性能比面向过程低。
3.2 面向对象三大特性
3.2.1 面向对象的特征有哪些方面
抽象:将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象。
- 封装:把类的属性私有化,同时对外界提供可被访问的方法。
- 继承:继承是使用已存在的类作为基础建立新的类的技术,新的类拥有父类的属性和方法,也可以定义自己的属性和方法。通过继承可以很方便地服用以前的代码。
- 多态:多态就是指程序中定义的引用变量所指的具体类型和通过该引用变量发出的方法调用在编译时不确定,而是在程序运行期间才能确定,即一个引用变量到底会指向哪个具体的实例对象,方法到底是哪个对象的方法必须在运行期间才能决定。
3.2.2 什么是多态机制?Java语言是如何实现多态的?
多态就是指程序中定义的引用变量所指的具体类型和通过该引用变量发出的方法调用在编译时不确定,而是在程序运行期间才能确定,即一个引用变量到底会指向哪个具体的实例对象,方法到底是哪个对象的方法必须在运行期间才能决定。即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。多态有编译时多态和运行时多态,编译时多态主要指方法重载,运行时多态是动态绑定,一般说多态就是指运行时多态。
多态的实现
Java中实现多态的三个必要条件:继承或实现接口、重写、向上转型。
继承:在多态中必须存在有继承关系的子类和父类中,或者是接口和接口的实现类中。
重写:子类对父类的方法进行重新定义,在调用时就会调用子类的方法。
向上转型:在多态中需要将子类的引用赋给父类的类型。这样才能调用父类和对用的子类中的方法。
多态遵循的一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型决定了调用谁的方法,但是这个被调用的方法必须在超类中定义过,也就是说被子类覆盖了的方法。
具体案例分析:
public class A {
public String show(D obj) {
return ("A and D");
}
public String show(A obj) {
return ("A and A");
}
}
public class B extends A{
public String show(B obj){
return ("B and B");
}
public String show(A obj){
return ("B and A");
}
}
public class C extends B{
}
public class D extends B{
}
public class Test {
public static void main(String[] args) {
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();
//this.show(o)-->super.show(o)-->this.show((super)o)-->super.show((super)o)
System.out.println("1--" + a1.show(b));//A.show(B)-->Object.show(B)-->A.show(A) = A and A
System.out.println("2--" + a1.show(c));//A.show(C)-->Object.show(C)-->A.show(B)-->A.show(A) = A and A
System.out.println("3--" + a1.show(d));//A.show(D)-->Object.show(D)-->A.show(D) = A and D
System.out.println("4--" + a2.show(b));//A.show(B)-->Object.show(B)-->A.show(A)此时发现a2引用的是B类的一个对象,并且B类覆盖了A的show(A obj)方法,所以调用B.show(A) = B and A
System.out.println("5--" + a2.show(c));//A.show(C)-->Object.show(C)-->A.show(B)-->A.show(A)此时发现a2引用的是B类的一个对象,并且B类覆盖了A的show(A obj)方法,所以调用B.show(A) = B and A
System.out.println("6--" + a2.show(d));//A.show(D) A中有show(D)的方法,由于a2引用的是B类的一个对象,所以再看B是否覆盖了show(D),如果覆盖了就执行B的方法,否则就执行A的方法,由于这里没覆盖所以执行A.show(D) = A and D
System.out.println("7--" + b.show(b));//B.show(B) = B and B
System.out.println("8--" + b.show(c));//B.show(C)-->A.show(C)-->B.show(B) = B and B
System.out.println("9--" + b.show(d));//B.show(D)-->A.show(D) = A and D
}
}
输出:
1--A and A
2--A and A
3--A and D
4--B and A
5--B and A
6--A and D
7--B and B
8--B and B
9--A and D
方法调用的优先级:this.show(o)–>super.show(o)–>this.show((super)o)–>super.show((super)o)
以5为例进行分析,a2.show©,a2是A类型的引用,所以this代表A,也就是this.show(o)=A.show©,在A中没找到show©的方法,所以根据调用链往下找,来到了super.show(o),A没有父类,所以继续往下找,来到了this.show((super)o)=A.show(B),没找到,继续找B的父类,也就是找A.show(A),发现A中有show(A)的方法,由于a2指向的是B类的对象,这时会继续寻找B类中是否覆盖了A类中的show(A)方法,如果有则执行B.show(A),否则执行A.show(A),在这里B覆盖了show(A)方法,所以执行B.show(A),所以结果是B and A。
3.2.3 面向对象五大基本原则是什么
- 单一职责原则:类的功能要单一
- 开放封闭原则:对扩展开放,对修改封闭。想要增加功能可以,想要修改功,不好意思,不让。
- 里氏替换原则:子类可以替换父类出现在父类能够出现的地方。
- 依赖倒置原则:高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖抽象。
- 接口分离原则:设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。
3.3 类与接口
3.3.1 抽象类和接口的区别
抽象类是将类的通用性进行抽象,是一种模板设计;接口时将行为进行抽象,是一种行为的规范。
相同点:
- 接口和抽象类都不能实例化。
- 都包含抽象方法。
不同点:

在接口和抽象类的选择上要遵循以下原则:
- 行为模型应该总是通过接口而不是抽象类定义,所以优先选择接口。
- 如果需要定义子类行为,又要为子类提供通用的功能,优先选择抽象类。
3.3.2 普通类和抽象类有哪些区别?
- 普通类不能包含抽象方法,抽象类既可以包含普通方法又可以包含抽象方法。
- 抽象类不能被实例化。
3.3.3 抽象类能使用 final 修饰吗?
不能。抽象类就是用来被继承的,如果用final修饰就不能被继承了,违反了抽象类设计的初衷。
3.3.4 创建一个对象用什么关键字?对象实例与对象引用有何不同?
new关键字。对象实例在堆内存中,对象引用在栈内存中。一个对象引用可以指向0或1个对象,一个对象可以被多个引用指向。
3.4 变量与方法
3.4.1 成员变量与局部变量的区别
变量: 在程序执行的过程中,在某个范围内其值可以发生改变的量。本质上是内存中的一块区域。
成员变量: 在类内部,方法外部定义的变量。
局部变量: 在方法内部定义的变量。
区别:

3.4.2 在Java中定义一个不做事且没有参数的构造方法的作用
如果父类中有构造方法,但是没有无参构造方法,在子类中的构造方法中如果没有使用super来调用父类的带参构造方法,这种情况下会发生编译错误。解决办法就是要么是在子类的构造方法中调用父类的带参构造方法,要么在父类中定义无参构造方法。
3.4.3 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?
帮助子类做初始化工作。
3.4.4 一个类的构造方法的作用是什么?若一个类没有声明构造方法,该程序能正确执行吗?为什么?
构造函数用于创建类的对象,完成对象的初始化工作。可以执行。因为一个类如果没有声明构造方法,也会有一个隐藏的默认的不带参数的构造方法。
3.4.5 构造方法有哪些特性?
- 方法名与类名相同。
- 没有返回值,不能用void修饰。
- 生成类的对象时自动执行。
- 构造方法可以被重载。
3.4.6 静态变量和实例变量区别
静态变量: 静态变量是属于类的变量,不属于任何一个类的实例,在内存中只有一份,JVM在类初次加载时分配内存空间。
实例变量: 实例变量是属于对象的,每次创建对象,都会为对象分配变量内存空间。
3.4.7 静态方法和实例方法有何不同?
- 静态方法使用“类名.方法名”的方式调用,实例方法使用“对象名.方法名”的方式调用。
- 静态方法只能使用本类中静态成员,实例方法无此限制。
3.4.8 在一个静态方法内调用一个本对象非静态成员为什么是非法的?
静态方法在任意对象实例化之前就已经存在,而非静态成员只有在对象实例化之后才会分配内存,然后通过实例对象引用来进行访问。也就是说静态方法在调用非静态成员时,这个对象实例都不存在,非静态成员当然更不存在。
3.5 内部类
内部类的详解请见我的系列文章:Java八股文系列五:内部类
3.6 重写与重载
3.6.1 构造器(constructor)是否可被重写(override)
构造方法不能被重写,但可以被重载。
3.6.2 重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
重载和重写都能实现多态性。重载实现的是编译时多态,重写实现的是运行时多态,重写是真正的多态。
- 重载: 发生在同一个类中,方法名相同参数列表不同(参数的个数不同、类型不同、顺序不同),与方法的返回值类型和访问修饰符无关。
- 重写: 发生在父子类中,方法名、参数列表必须相同,返回值的类型要小于等于父类,抛出的异常类要小于等于父类的异常类,访问修饰符大于等于父类(里氏代换原则)。如果父类的方法是private的,则子类不能重写。
3.7 对象相等判断
3.7.1 == 和 equals 的区别是什么
- ==: 它的作用是比较两个对象的内存地址是否相等。即判断两个对象是否是同一个对象(基本类型比较的是值,引用类型比较的是内存地址)。
- equals(): 也是比较两个对象是否相等,分两种情况:没有覆盖equals()方法和覆盖了equals()方法。没有覆盖equals()方法等价于“==”比较这个两个对象,覆盖了equals()方法一般是比较对象的内容是否相同。
3.7.2 hashCode() 与 equals()
1. hashCode()的作用
hashCode()是所有对象的方法,它返回一个int型的整数,叫做哈希码,这个哈希码的作用是确定该对象在哈希表中的位置。也就是说hashCode()在散列表中才有用,在其他情况下没用。 进一步说就是将对象放入HashMap、HashSet等散列表结构的类中,要根据HashCod()来确定对象在数组中的位置。
2. 为什么要有HashCode(),为什么要根据HashCode()来确定对象在散列表中的位置?
HashCode()是一个native方法,hashCode() 的默认⾏为是对堆上的对象产⽣独特值。如果没有重写HashCode()方法,则该类的两个对象无论如何都不会相等(即使两个对象的内容完全相等)。
以HashSet为例:假设HashSet中已经有1万个元素,插入第10001个元素时如何确定这个元素不与前1万个元素重复呢?“用equals()方法逐个与前1万个元素比较”,这样可以但是效率很低。可不可以用HashCode()呢,如果HashCode()不被重写即使两个对象的内容完全相等那么这两个对象的HashCode()也不相等,这样两个内容相等的对象算出的位置却不同,就失去了HashSet的元素不重复的意义。所以要重写HashCode(),来保证两个内容相等的对象的HashCode()一定要相等。如果重写了HashCode(),保证了两个内容相等的对象的HashCode()一定相等,就可以判断是相等的对象了吗?不可以,因为内容不同的对象的HashCode()也可能相同,所以HashCode()相同的话还要用equals()再比较一下,如果内容真的相同,那么就是相等的对象。
3. hashCode() 与 equals()
如果不将对象放入像HashMap这样的散列表结构中,hashCode() 与 equals()没有任何关系。
将对象放入像HashMap这样的散列表结构中,hashCode() 与 equals()是有关系的。
- 如果两个对象相等,那么它们的HashCode()值一定相同。这里的相等是指通过equals()比较两个对象时返回true。
- 如果两个对象的HashCode()相等,那它们不一定相等。因为在散列表中会存在hash冲突,即两个不同的键值对哈希值相等。
因此,equals()方法被覆盖过,hashCode()方法也要被覆盖。
3.7.3 对象的相等与指向他们的引用相等,两者有什么不同?
- 对象相等:比较内存中存放的内容是否相等。
- 引用相等:比较他们是否指向同一块内存地址。
3.8 值传递
3.8.1 当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递
是值传递。Java的方法调用只支持值传递。当一个对象实例作为参数被传递到方法中时,参数的值就是该对象的引用,就是一个地址,形参是这个地址的一份拷贝。对象的属性可以在被调用的过程中被改变,但是对这个对象引用的改变是不会影响到调用者的,也就是说如果在方法中改变了这个对象的引用,将这个引用指向了一个新的地址,那么传过来的那个引用还是指向原来的地址。
3.8.2 为什么 Java 中只有值传递
- 按值调用(值传递):表示方法接收的是调用者提供的值,实际参数的副本将传递给形式参数,也就是说方法得到的是所有参数值的一个拷贝。
- 按引用调用(引用传递):通过引用调用(也称为引用传递)是一种评估策略,其中函数接收对用作参数的变量的隐式引用,而不是其值的副本。
值传递和引用传递最本质的区别就是:值传递不会修该之前的值,引用传递会修改原来的值。这里的“原来的值”有两只情况,一种是参数是基本数据类型,另一种是引用类型。如果参数类型是引用类型,“引用传递会修改引用类型的值”的意思就是引用传递会修改原来引用的地址,“值传递不会修改引用类型的值”的意思就是值传递不会修改原来引用的地址,而通过引用修改对应内存地址中的内容是引用的最基本的功能,不能叫引用传递,所以是不是引用传递关键就在于看能不能修改原来引用的地址。
当参数是基本数据类型时:
public static void main(String[] args) {
int num1 = 10;
int num2 = 20;
swap(num1, num2);
System.out.println("num1 = " + num1);
System.out.println("num2 = " + num2);
}
public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("a = " + a);
System.out.println("b = " + b);
}
输出:
a = 20
b = 10
num1 = 10
num2 = 20
可以看到num1和num2的值没有被改变,所以基本数据类型是值传递。
当参数是引用类型时:
public class Foo {
private String name;
public Foo(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static void changeReference(Foo a) {
Foo b = new Foo("b");
a = b;
}
public static void modifyReference(Foo c) {
c.setName("c");
}
public static void main(String[] args) {
Foo f = new Foo("f");
Foo old = f;
System.out.println(f == old);//true
changeReference(f); // 将f指向一个新的对象
System.out.println(f.getName().equals("f"));//true
System.out.println(f.getName().equals("b"));//false
System.out.println(f == old);//true
modifyReference(f); // 修改f的内容
}
}
changeReference(Foo a)方法将接收到的Foo类型对象的引用指向了一个新的地址,执行完此方法后结果发现引用类型f的指针并没有指向其他的地址,也就是说当参数是引用类型时也没有修改指针的值,所以也是值传递。以下是大致流程:
- 声明一个Foo类型的引用f,并指向一个name属性为"f"的对象。
Foo f = new Foo("f");

- 在方法形参上声明一个Foo类型的引用a,并指向null。
public static void changeReference(Foo a)

- 当调用changeReference(f)方法时,将引用a指向f所指向的对象。
changeReference(f);

- 声明一个Foo类型的引用b,并指向一个name属性为"b"的对象。
Foo b = new Foo("b");

- 将a指向b所指向的对象。
a=b;

- 当调用modifyReference(f)方法时,将引用c指向f所指向的对象。

- 当调用c.setName(“c”)时,c所指向的对象的name将被改成"c"。

下面总结一下Java中参数传递的情况:
- 一个方法不能修改基本数据类型的参数
- 一个方法可以改变一个对象的参数的状态
- 一个方法不能让参数对象引用一个新的对象
3.8.3 值传递和引用传递有什么区别

四、IO流
4.1 java 中 IO 流分为几种?
- 按照流的流向分:输入流和输出流。
- 按照操作单元分:字节流和字符流。
- 按照流的角色分:节点流和处理流。
Java IO流的40多个类都是从下面4个抽象类中派生出来的:
- InputStream/Reader:所有输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流。
按操作对象分类结构图:

4.2 既然有了字节流,为什么还要有字符流?
字符流是由Java虚拟机将字节转换得到的,这个过程非常耗时,并且如果我们不知道编码类型就很容易出现乱码问题。所以,就直接提供了一个字符流来操作字符。
像音视频文件、图片文件用字节流操作,涉及到字符的话用字符流操作。
4.3 BIO,NIO,AIO 有什么区别?
- BIO(Blocking I/O): 同步阻塞IO。数据的读写必须阻塞在一个线程内等待其完成,在活动连接数不是特别高的情况下,这种模型还是不错的,可以让每一个连接专注于自己的I/O并且编程模型简单。但是当连接数达到数十万的时候,这种模型是无能为力的。
- AIO: 异步非阻塞的IO模型。异步IO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会阻塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
- NIO(New I/O或Non-blocking I/O): 同步非阻塞IO。它支持面向缓冲的,基于通道的I/O操作方法。NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO的非阻塞模式来开发.
五、反射
关于反射,具体请看我的系列文章 Java八股文系列三:反射
六、常用API
6.1 String相关
6.1.1 什么是字符串常量池?
字符串常量池存在于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块内存空间存储相同的字符串,在创建字符串时JVM会首先检查字符串常量池,如果该字符串已经存在于常量池中,则返回它的引用,如果不存在,则实例化一个字符串放到常量池中,并返回其引用。
6.1.2 String 是最基本的数据类型吗
不是,Java中的基本数据类型只有8个:byte、short、int、long、float、double、char、boolean,而String是一个char类型的数组。
6.1.3 String有哪些特性
- 不变性。String底层是private final char value[],一个用final修饰的char数组。对它进行任何操作,其实是创建一个新的对象,再把引用指向该对象。
- 常量池优化。String对象创建后,会在常量池里进行缓存,如果下次创建相同的对象,会直接返回缓存的引用。
- String类不能被继承。String是一个被final修饰的类。
6.1.4 String为什么是不可变的?
- String的底层是一个final修饰的char型数组,不可变的是这个数组中的字符,但是我们可以通过改变这个数组的引用,使其指向其他的数组。
String str = "Hello";
str = str + " World";
System.out.println("str=" + str);
str指向了一个新的String对象"Hello World",而原来在内存中的"Hello"还是"Hello"。
- 可以通过反射技术来打破String的不可变。
public class Test {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s = "Hello World";
System.out.println("before s = " + s);
//获取String的value字段
Field values = String.class.getDeclaredField("value");
//改变访问权限
values.setAccessible(true);
//获取value属性的值
char[] o = (char[]) values.get(s);
//修改value所引用的数组中的第五个字符
o[5] = '_';
System.out.println("after s = " + s);
}
}
输出:
before s = Hello World
after s = Hello_World
- String为什么被设计成不可变的?
- 缓存的需要:被缓存的String字面量需要在多个线程之间共享,如果一个客户端将"test"修改为"prod",其他客户端也会得到这个结果,但显然并不想要这个接口,为了避免这个风险,String被设计成不可变的。
- HashMap的需要:HashMap的key通常都是String类型,如果String可以修改,其hashcode也将会改变,这样导致在HashMap中找不到原来的value。
总之,考虑到安全性和字符串常量池等原因,String被设计成不可变的。
6.1.5 是否可以继承 String 类
不可以被继承。String是final类。
6.1.6 String str="i"与 String str=new String(“i”)一样吗?
不一样。str="i"是将str指向字符串常量池中的地址,str=new String(“i”)是将str指向堆中的地址。这两个地址不一样。
String对象创建的两种方式:
- String a=“abc”:直接在字符串常量池中创建"a",并返回常量池中的引用,如果常量池中已经有了"a",则直接返回"a"的引用。
- String a=new String(“abc”):在堆中创建一个"abc"对象,并返回一个在堆中的引用,并将"abc"写入常量池中,如果常量池中已经有了"abc"则不会再常量池中创建一个新的"abc"。
6.1.7 String s = new String(“xyz”);创建了几个字符串对象
两个对象。一个是在字符串常量池中的"xyz"对象,一个是在堆中的"xyz"对象,s指向的是堆中的对象。
- String类型的变量相加时,其实是使用的StringBuilder.append来完成的,会在堆上创建新的对象。
String s1 = "abc";
String s2 = "a";
String s3 = "bc";
String s4 = s2 + s3;
System.out.println(s1 == s4);//false
- String类型的常量相加时,编译器会进行优化,将引用直接替换成字符串的值。
String S1 = "abc";
final String S2 = "a";
final String S3 = "bc";
String S4 = S2 + S3;
System.out.println(S1 == S4);//true
- String类型变量和String的字面量相加时,是使用的StringBuilder.append来完成的。
String str1 = "abcd"; // 常量池创建"abcd"
String str4 = "ab"; // 常量池已经有了"ab"
str4 += "cd"; // str4+"cd"连接的字符串编译器不能优化,所以此时str4指向堆
System.out.println(str1 == str4); // false
- String类型的字面量相加时
String fg = "fg";
System.out.println("f" + "g" == fg);//true 生成"ab",发现常量池中已经存在"ab",所以直接指向"ab"。
6.1.8 在使用 HashMap 的时候,用 String 做 key 有什么好处?
HashMap是通过key的hashcode来计算value的存储位置的,由于String是不可变的,所以String的hashcode也是不可变的,就可将它的hashcode缓存下来,不需要再次计算,所以效率高。
6.1.9 String和StringBuffer、StringBuilder的区别是什么?
- 可变与不可变。
- String的底层是使用final修饰的char数组,所以String不可变。
- StringBuilder和StringBuilder都继承自AbstractStringBuilder类,AbstractStringBuilder类使用的是没有final修饰的char数组,所以可变。
- 是否多线程安全。
- String对象是不可变的,也就是常量,显然是线程安全的。
- StringBuilder对象可变,并且方法也没有用到同步锁,所以是非线程安全的。
- StringBuffer对象可变,但是方法加了同步锁,所以是线程安全的。
- 性能
- 由于String类型进行改变时,每次都会生成一个新的对象,而那两个是对对象本身进行操作,不会生成新的对象,所以String的性能最差。
6.2 包装类相关
6.2.1 自动装箱与拆箱
- 装箱: 将基本数据类型用它们的引用类型包装起来。
- 拆箱: 将包装类型转换为基本类型。
6.2.2 int 和 Integer 有什么区别
int是基本数据类型,Integer是其包装类型。
6.2.3 Integer a= 127 与 Integer b = 127相等吗
- 对于基本数据类型:==比较的是值。
- 对于对象引用类型:==比较的是内存地址。
- Java的6种基本类型的包装类实现了常量池技术,即Byte、Short、Integer、Long、Character、Boolean,这6种包装类型会有相应的缓存数据,但是超出范围仍然会创建新的对象。Byte、Short、Integer、Long缓存[-128,127]间的数据,Character缓存[0,127]之间的数据,Boolean缓存true和false两个对象。
- 两个浮点型的包装类Float、Double没有常量池。
Integer t = 127;
Integer t1 = 127;
Integer t2 = 128;
Integer t3 = 128;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println(t == t1);
System.out.println(t2 == t3);
System.out.println("i4=i5 " + (i4 == i5));
System.out.println("i4=i5+i6 " + (i4 == i5 + i6));
输出:
true
false
i4=i5 false
i4=i5+i6 true
Integer.valueOf源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
解释:Integer t = 127;会在编译时直接将代码封装成Integer.valueOf(127),这叫自动装箱,Integer.valueOf(127)的源码在上方,于此可见,由于t和t1的127都在常量池中,所以t和t1都引用常量池中的同一块内存。t2和t3的128不在常量池中,所以t2和t3都分别创建了新的对象。解释i4=i5,new Integer(40)是手动装箱,不会调用Integer.valueOf(40)的方法,所以i4和i5分别直接new了两个新的对象。解释i4=i5+i6,Integer的加操作会将包装类型自动拆箱成int型,即i4==40,然后Integer对象无法与数值进行直接比较,所以将i4自动拆箱成int型的40,最后转化成数值比较。
关注Java技术吧公众号,会持续更新深入理解Java开发涉及到的各种技术的文章和面试题总结。

6934



被折叠的 条评论
为什么被折叠?



