基础概念与常识
Java 语言有哪些特点?
- 简单的
语法与 C、C++语言很接近,程序员很容易上手使用。同时,省略了操作符重载、多继承、自动类型转换等内容。相较于 C++,没有指针的概念,并且内置的自动垃圾收集机制简化了程序设计者的内存管理工作。 - 面向对象的
Java 语言提供了类、接口和继承等概念。可以将一个对象抽象为一个类,类中可以封装各种变量以及方法,从而对对象进行操作,实现模块化和信息隐藏。Java 提供了类之间的单继承和接口的多继承,通过继承,子类可以使用父类的方法,实现代码的复用。 - 健壮性
Java 在编译和运行程序时都会对代码进行检查,以消除问题产生。提供垃圾处理机制进行内存管理,防止程序员在管理内存时出错。异常处理机制可以帮助合理处理错误情况。 - 安全性
一方面在 Java 语言里,指针和释放内存等 C++功能被删除,避免了非法内存操作。另一方面当 Java 用来创建浏览器时,语言功能和浏览器本身提供的功能结合起来,使更安全。 - 可移植性/平台无关性
平台无关性就是一种语言在不同的计算机设备上都能够运行,不受平台约束。Java 针对不同的计算机设备提供了对应的 Java 虚拟机,Java 虚拟机可以将 Java 编译后的 Class 文件(字节码文件)转换为能被对应平台执行的二进制文件,从而实现了平台无关性。 - 解释型的
Java 解释器直接对 Java 字节码进行解释执行,执行过程中需要的类在联接阶段被载入到运行环境中。 - 高性能
和其它解释执行的语言不同, Java 字节码的设计使之能很容易地直接转换成对应于特定平台的机器码 , 从而得到较高的性能。 - 支持分布式
Java 语言是支持网络应用开发的,在基本的 Java 应用编程接口中有一个 Java. net 包,提供了用于网络应用编程的类库,包括 URL、URLConnection、Socket、ServerSocket 等。 - 支持多线程
Java 语言支持多个线程的同时执行,并提供多线程之间的同步机制。 - 动态的
Java 语言的设计目标之一是适应于动态变化的环境。Java 程序需要的类能够动态地被载入到运行环境,也可以通过网络来载入所需要的类。这也有利于软件的升级。另外,Java 中的类有一个运行时刻的表示,能进行运行时刻的类型检查。
最初宣传口号:“Write Once, Run Anywhere(一次编写,随处运行)”。但是跨平台不再是 Java 语言最大的优势,当前虚拟化技术已经非常成熟,比如通过 Docker 就很容易实现跨平台了。当前 Java 最大的优势应该是 Java 的生态系统。
JDK vs JRE vs JVM
JDK > JRE > JVM
JVM
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现,目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
JVM 并不是只有一种!只要满足 JVM 规范,每个公司、组织或者个人都可以开发自己的专属 JVM。
JDK 和 JRE
JDK 是 Java Development Kit 缩写。它是功能齐全的 Java SDK。
JDK = JRE + 编译器(Javac)+工具(如 Javadoc 和 jdb)等等。它能够创建和编译程序。
JRE 是 (Java Runtime Enviroment) Java 运行时环境;它是运行已编译 Java 程序(Class 文件)所需所有内容的集合。
JRE = Java 虚拟机(JVM)+ Java 类库 + Java 命令和其他的一些基础构件。但是,它不能用于创建新程序。
如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。
如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。

但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。
什么是字节码? 采用字节码的好处是什么?
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以, Java 程序运行时相对来说还是高效的(不过,和 C++,Rust,Go 等语言还是有一定差距的)。
Java 程序从源代码到运行的过程如下图所示:
graph LR A(.Java) --Javac编译-->B(.class) B --JVM-->C(机器可理解代码) C --> D(OS)
我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的 (也就是所谓的热点代码),所以后面引进了 JIT(just-in-time compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言 。
HotSpot 采用了惰性评估 (Lazy Evaluation) 的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式 AOT (Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协作使用。但是,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。
为什么说 Java 语言“编译与解释并存”?
其实这个问题我们讲字节码的时候已经提到过,因为比较重要,所以我们这里再提一下。
我们可以将高级编程语言按照程序的执行方式分为两种:
- 编译型 :编译型语言 会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
- 解释型 :解释型语言会通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。
根据维基百科介绍:
为了改善编译语言的效率而发展出的即时编译技术,已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成字节码。到执行期时,再将字节码直译,之后执行。Java 与 LLVM 是这种技术的代表产物。
为什么说 Java 语言“编译与解释并存”?
这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码( .class 文件),这种字节码必须由 Java 解释器来解释执行。
Java 和 C++ 的区别?
- 都是面向对象的语言,都支持封装、继承和多态
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
- C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。
- C++支持默认函数参数,而 Java 不支持。
- C++支持 goto 语句,而 Java 不提供 goto 语句(但 Java 中 goto 是保留关键字)。
- C++支持自动强制类型转换,这会导致程序的不安全;Java 不支持自动强转,必须由开发人员进行显式地强制类型转换。
- Java 有自动内存管理垃圾回收机制 (GC),不需要程序员手动释放无用内存。
- Java 有反射机制,允许程序在运行时进行自我检查,同时也允许对其内部的成员进行操作。C++没有提供这样的特性。
- Java 为解释性语言,其运行过程为:程序源代码经过 Java 编译器编译成字节码,然后由 JVM 解释执行。而 C/C++为编译型语言,源代码经过编译和链接后生成可执行的二进制代码。因此,Java 的执行速度比 C/C++慢,但是 Java 能够跨平台执行,而 C/C++不能。
- Java 为纯面向对象语言,所有代码(包括函数、变量等)必须在类中实现,除基本数据类型(包括 int、float 等)外,所有类型都是类。此外,Java 语言中不存在全局变量或全局函数,而 C++兼具面向对象和面向过程变成的特点,可以定义全局变量和全局函数。
- Java 语言中没有指针的概念,这有效防止了 C/C++语言中操作指针可能引起的系统问题,从而使程序变得更加安全。
- Java 字符串类作为 Java 语言的一部分定义,而不是作为外加的延伸部分,在整个系统中建立字符串和访问字符串元素的方法是一致的。C++不支持字符串变量,使用“Null”终止符代表字符串的结束。
Java 8 新特性
- Interface 支持默认方法
- 函数式接口
- Lambda 表达式
- Optional 类
- Stream 流
基本语法
标识符和关键字的区别是什么?
标识符就是一个名字。关键字是被赋予特殊含义的标识符。
比如,“警察局”这个名字已经被赋予了特殊的含义,所以如果你开一家店,店的名字不能叫“警察局”。
Java 中有哪些常见的关键字?
| 分类 | 关键字 | ||||||
|---|---|---|---|---|---|---|---|
| 访问控制 | private | protected | public | ||||
| 类,方法和变量修饰符 | abstract | class | extends | final | implements | interface | native |
| new | static | strictfp | synchronized | transient | volatile | ||
| 程序控制 | break | continue | return | do | while | if | else |
| for | instanceof | switch | case | default | |||
| 错误处理 | try | catch | throw | throws | finally | ||
| 包相关 | import | package | |||||
| 基本类型 | boolean | byte | char | double | float | int | long |
| short | null | true | false | ||||
| 变量引用 | super | this | void | ||||
| 保留字 | goto | const |
访问修饰符 public、private、protected、以及不写(默认)时的区别
Java 中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。
- default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
- private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
- public : 对所有类可见。使用对象:类、接口、变量、方法
- protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
protected 需要从以下两个点来分析说明:
- 子类与基类在同一包中:被声明为 protected 的变量、方法和构造器能被同一个包中的任何其他类访问;
- 子类与基类不在同一包中:那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能访问基类实例的 protected 方法。
switch 是否能作用在 byte、long、String 上
Java5 以前 switch (expr) 中,expr 只能是 byte、short、char、int。
从 Java 5 开始,Java 中引入了枚举类型, expr 也可以是 enum 类型。
从 Java 7 开始,expr 还可以是字符串 (String),但是长整型 (long) 在目前所有的版本中都是不可以的。
final、finally、finalize 的区别?
final 用于修饰变量、方法和类。
- final 变量:被修饰的变量不可变,不可变分为引用不可变和对象不可变,final 指的是引用不可变,final 修饰的变量必须初始化,通常称被修饰的变量为常量。
- final 方法:被修饰的方法不允许任何子类重写,子类可以使用该方法。
- final 类:被修饰的类不能被继承,所有方法不能被重写。
finally 作为异常处理的一部分,它只能在 try/catch 语句中,并且附带一个语句块表示这段语句最终一定被执行(无论是否抛出异常),经常被用在需要释放资源的情况下,System.exit(0) 可以阻断 finally 执行。
finalize 是在 Java. lang. Object 里定义的方法,也就是说每一个对象都有这么个方法,这个方法在 gc 启动,该对象被回收的时候被调用. 一个对象的 finalize 方法只会被调用一次,finalize 被调用不一定会立即回收该对象,所以有可能调用 finalize 后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会再次调用 finalize 了,进而产生问题,因此不推荐使用 finalize 方法。
static 关键字
”static”关键字是什么意思?Java 中是否可以覆盖 (override) 一个 private 或者是 static 的方法?
“static”关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问。
Java 中 static 方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而 static 方法是编译时静态绑定的。static 方法跟类的任何实例都不相关,所以概念上不适用。
是否可以在 static 环境中访问非 static 变量?
static 变量在 Java 中是属于类的,它在所有的实例中的值是一样的。当类被 Java 虚拟机载入的时候,会对 static 变量进行初始化。如果你的代码尝试不用实例来访问非 static 的变量,编译器会报错,因为这些变量还没有被创建出来,还没有跟任何实例关联上。
static 静态方法能不能引用非静态资源?
不能,new 的时候才会产生的东西,对于初始化后就存在的静态资源来说,根本不认识它。
静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。
非静态成员属于某实例对象,只有在对象实例化之后才存在,然后通过类的实例对象去访问。
在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
static 静态方法里面能不能引用静态资源?
可以,因为都是类初始化的时候加载的,大家相互都认识。
非静态方法里面能不能引用静态资源?
可以,非静态方法就是实例方法,那是 new 之后才产生的,那么属于类的内容它都认识。
静态方法和实例方法有何不同?
1、调用方式
静态方法:类名.方法名,不推荐 对象.方法名 的方式
实例方法: 对象.方法名 的方式
2、访问类成员的限制
静态方法:静态成员(静态成员变量+方法)
实例方法:静态+非静态成员
3. 加锁时锁住的对象
静态方法:类对象
实例方法:实例对象
静态初始化块、构造初始化块、构造方法的执行顺序是什么?
Static 静态代码块以及各代码块之间的执行顺序 - 掘金
public class BaseThree extends BaseTwo {
public BaseThree() {
System.out.println("构造器");
}
{
System.out.println("初始化块");
}
static {
System.out.println("静态初始化块");
}
}第一次创建类实例的时候,静态初始化块 → 初始化块 → 构造方法。
第二次创建类实例的时候,初始化块 → 构造方法。
继承关系中,假设 C 继承 B,B 继承 A。
第一次创建 C 类实例的时候,A-B-C 静态初始化块 → A 初始化块、A 构造器 → B 初始化块、B 构造器 → C 初始化块、C 构造器。
第二次创建 C 类实例的时候,A 初始化块、A 构造器 → B 初始化块、B 构造器 → C 初始化块、C 构造器。
基本数据类型
Java 中的几种基本数据类型是什么?对应的包装类型是什么?各自占用多少字节呢?
Java 中有 8 种基本数据类型,分别为:6 种数字类型,1 种字符类型,1 种布尔型。
| 基本类型 | 位数 | 字节 | 默认值 |
|---|---|---|---|
boolean | 1 | false | |
char | 16 | 2 | ’u0000’ |
byte | 8 | 1 | 0 |
short | 16 | 2 | 0 |
int | 32 | 4 | 0 |
long | 64 | 8 | 0L |
float | 32 | 4 | 0f |
double | 64 | 8 | 0d |
另外,对于 boolean,官方文档未明确定义,它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位,但是实际中会考虑计算机高效存储因素。
注意:
- Java 里使用
long类型的数据一定要在数值后面加上 L,否则将作为整型解析。 char a = 'h'单引号,String a = "hello": 双引号。
这八种基本类型都有对应的包装类分别为:Byte、 Short、 Integer、 Long、 Float、 Double、 Character、 Boolean。包装类型不赋值就是 Null ,而基本类型有默认值且不是 Null。
另外,这个问题建议还可以先从 JVM 层面来分析。
基本数据类型直接存放在 Java 虚拟机栈中的局部变量表中,而包装类型属于对象类型,我们知道对象实例都存在于堆中。相比于对象类型,基本数据类型占用的空间非常小。
《深入理解 Java 虚拟机》 :局部变量表主要存放了编译期可知的基本数据类型 (boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
基本数据类型存放位置
存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
为什么说是几乎所有对象实例都存在于堆中呢? 这是因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存。
⚠️ 注意:基本数据类型存放在栈中是一个常见的误区! 基本数据类型的存储位置取决于它们的作用域和声明方式。如果它们是局部变量,那么它们会存放在栈中;如果它们是成员变量,那么它们会存放在堆中。
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 修饰局部变量
}
}自动装箱与拆箱
- 装箱:将基本类型用它们对应的引用类型包装起来;
Integer i = 10; - 拆箱:将包装类型转换为基本数据类型;
int n = i;
装箱其实就是调用了包装类的 valueOf() 方法,拆箱其实就是调用了 xxxValue() 方法。
Integer i = 10等价于Integer i = Integer.valueOf(10)int n = i等价于int n = i.intValue();
浮点数精度丢失+如何解决?
BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。
4 种基本类型的包装类和常量池
Java 基本类型的包装类的大部分都实现了常量池技术。Byte, Short, Integer, Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0, 127] 范围的缓存数据,Boolean 直接返回 True Or False。
Integer 缓存源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
}** Character 缓存源码:**
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}
private static class CharacterCache {
private CharacterCache(){}
static final Character cache[] = new Character[127 + 1];
static {
for (int i = 0; i < cache.length; i++)
cache[i] = new Character((char)i);
}
}** Boolean 缓存源码:**
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。
两种浮点数类型的包装类 Float, Double 并没有实现常量池技术。
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true
Float i11 = 333f;
Float i22 = 333f;
System.out.println(i11 == i22);// 输出 false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false下面我们来看一下问题。下面的代码的输出结果是 true 还是 flase 呢?
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);Integer i1=40 这一行代码会发生装箱,也就是说这行代码等价于 Integer i1=Integer.valueOf(40) 。因此,i1 直接使用的是常量池中的对象。而 Integer i1 = new Integer(40) 会直接创建新的对象。因此,答案是 false 。
记住:所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
变量对比
Integer i = new Integer(10000);
Integer j = new Integer(10000);
System.out.print(i == j); //false
int a = 10000;
Integer b = new Integer(10000);
Integer c=10000;
System.out.println(a == b); // true
System.out.println(a == c); // true
Integer b = new Integer(10000);
Integer c=10000;
System.out.println(b == c); // false
Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true
Integer i = 128;
Integer j = 128;
System.out.print(i == j); //falseJava 变量
成员变量与局部变量的区别有哪些?
- 生命周期:成员变量 - 类对象 or 实例对象,局部变量 - 代码块
- 修饰符:成员变量 - public、static、final 等修饰符修饰,局部变量 - final 修饰
- 存储位置:成员变量 - 堆内存,局部变量 - 栈内存
- 默认值:成员变量 - 有默认值,局部变量 - 无默认值
- 加锁反应:成员变量 - 类对象 or 实例对象,局部变量 - 可能存在锁消除。
- 语法形式:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被
public,private,static等修饰符所修饰,而局部变量不能被访问控制修饰符及static所修饰;但是,成员变量和局部变量都能被final所修饰。 - 存储方式:从变量在内存中的存储方式来看,如果成员变量是使用
static修饰的,那么这个成员变量是属于类的,如果没有使用static修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 - 生存时间:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
- 默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被
final修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
静态变量作用
静态变量也就是被 static 关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。
静态变量是通过类名来访问的,例如 StaticVariableExample.staticVar(如果被 private 关键字修饰就无法这样访问了)。
public class StaticVariableExample {
// 静态变量
public static int staticVar = 0;
}通常情况下,静态变量会被 final 关键字修饰成为常量。
public class ConstantVariableExample {
// 常量
public static final int constantVar = 0;
}字符型常量和字符串常量的区别?
- 形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符
- 含义 : 字符常量相当于一个整型值 ( ASCII 值), 可以参加表达式运算; 字符串常量代表一个地址值 (该字符串在内存中存放位置)
- 占内存大小 : 字符常量只占 2 个字节; 字符串常量占若干个字节 (注意:
char在 Java 中占两个字节),
方法(函数)
为什么 Java 中只有值传递?
按值调用 (call by value) 表示方法接收的是调用者提供的值,按引用调用(call by reference) 表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。它用来描述各种程序设计语言(不只是 Java)中方法参数传递方式。
Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。
下面再总结一下 Java 中方法参数的使用情况:
- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
- 一个方法可以改变一个对象参数的状态,或者说内部变量。
- 一个方法不能让对象参数引用一个新的对象,因为对象参数传入的是引用地址,根据结论 1,单纯数据是不能改的。
重载和重写的区别
重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
重载
发生在同一个类或者父类和子类之间,方法名必须相同,参数必须不同,返回类型和访问修饰符可以不同。其中参数不同包括类型不同、个数不同、顺序不同。例如,一个类中有多种构造方法;求最大值可以传入两个对象,也可以传入一个数组。
重写
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
- 方法名、参数列表必须相同,返回值类型、抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
- 如果父类方法访问修饰符为
private/final/static则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。 - 构造方法无法被重写。
| 区别点 | 重载方法 | 重写方法 |
|---|---|---|
| 发生范围 | 同一个类 | 子类 |
| 参数列表 | 必须修改 | 一定不能修改 |
| 返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 |
| 异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; |
| 访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
| 发生阶段 | 编译期 | 运行期 |
关于 重写的返回值类型 这里需要额外多说明一下,上面的表述不太清晰准确:如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。
什么是可变长参数?
从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面这个方法就可以接受 0 个或者多个参数。
public static void method1(String... args) {
//......
}另外,可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数。
public static void method2(String arg1, String... args) {
//......
}遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢?
答案是会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。
我们通过下面这个例子来证明一下。
public class VariableLengthArgument {
public static void printVariable(String... args) {
for (String s : args) {
System.out.println(s);
}
}
public static void printVariable(String arg1, String arg2) {
System.out.println(arg1 + arg2);
}
public static void main(String[] args) {
printVariable("a", "b");
printVariable("a", "b", "c", "d");
}
}
另外,Java 的可变参数编译后实际会被转换成一个数组,我们看编译后生成的 class 文件就可以看出来了。
public class VariableLengthArgument {
public static void printVariable(String... args) {
String[] var1 = args;
int var2 = args.length;
for(int var3 = 0; var3 < var2; ++var3) {
String s = var1[var3];
System.out.println(s);
}
}
// ......
}Java 面向对象
深拷贝 vs 浅拷贝
- 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
- 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
clone()对于一维数组是深拷贝,对于多维数组是浅拷贝。
自定义:实现了 Cloneable 接口,重写了 clone() 方法。
面向对象和面向过程的区别
- 面向过程 :面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展。
- 面向对象 :面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,面向对象性能比面向过程低。
参见 issue : 面向过程 :面向过程性能比面向对象高??
这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java 性能差的主要原因并不是因为它是面向对象语言,而是 Java 是半编译语言,最终的执行代码并不是可以直接被 CPU 执行的二进制机械码。
而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比 Java 好。
创建一个对象用什么运算符? 对象实体与对象引用有何不同?
new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
对象的相等与指向他们的引用相等, 两者有什么不同?
对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。
一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?
构造方法主要作用是完成对类对象的初始化工作。
如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来,减少日后错误。
抽象类和接口的区别是什么?
抽象类能使用 final 修饰吗?
不能,定义抽象类就是让其他类继承的。
如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类。
Java 创建对象有哪几种方式?
- new 创建新对象
- 通过反射机制
- 采用 clone 机制
- 通过序列化机制
能否创建一个包含可变对象的不可变对象?
当然可以, 比如 final Person[] persons = new Persion[]{}.
persons 是不可变对象的引用, 但其数组中的 Person 实例却是可变的.
这种情况下需要特别谨慎, 不要共享可变对象的引用.
这种情况下, 如果数据需要变化时, 就返回原对象的一个拷贝.
构造方法有哪些特点?是否可被 override?
- 名字与类名相同。
- 没有返回值,但不能用 void 声明构造函数。
- 生成类的对象时自动执行,无需调用。
构造方法不能被 override(重写), 但是可以 overload(重载), 所以你可以看到一个类中有多个构造函数的情况。
面向对象三大特征、优点
封装
封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。就好像我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。如果属性不想被外界访问,我们大可不必提供方法给外界访问。。
继承
手机与小米手机之间的关系,小米手机继承手机类。保留相同特点、增加新特点、修改旧特点。可以提高代码的重用,程序的可维护性,节省大量创建新类的时间,提高我们的开发效率。
关于继承如下 3 点请记住:
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。(以后介绍)。
多态
多态,顾名思义,表示一个对象具有多种的状态。具体表现为父类的引用指向子类的实例。
多态的特点:
- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
- 多态不能调用“只在子类存在但在父类不存在”的方法;
优点:
- 封装:屏蔽内部具体实现,只暴露特定接口。
- 继承:简化代码。
- 多态:降低代码的耦合,例如一个函数的入参使用某父类接口,可以避免与子类对象耦合严重。
Java 语言是如何实现多态的?
本质上多态分两种:
- 编译时多态(又称静态多态)
- 运行时多态(又称动态多态)
例子:
- 方法重载,都是编译时多态。在编译期可根据参数的数据类型、个数以及次序来确定调用方法
- 方法重写,当子类对象引用自身类实例方法时,为编译时多态。但是当父类对象引用子类实例方法时 (也就是上转型对象时,父类声明,子类实例化),为运行时多态,因为此时只有在运行时才可以去匹配到对应方法进行调用。
例如,AbstractExecutorService抽象类的submit方法,该方法中调用了子类实现的execute方法。
接口和抽象类有什么共同点和区别?
接口和抽象类的共同点
- 实例化:接口和抽象类都不能直接实例化,只能被实现(接口)或继承(抽象类)后才能创建具体的对象。
- 抽象方法:接口和抽象类都可以包含抽象方法。抽象方法没有方法体,必须在子类或实现类中实现。
接口和抽象类的区别
- 设计目的:接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
- 继承和实现:一个类只能继承一个类(包括抽象类),因为 Java 不支持多继承。但一个类可以实现多个接口,一个接口也可以继承多个其他接口。
- 成员变量:接口中的成员变量只能是
public static final类型的,不能被修改且必须有初始值。抽象类的成员变量可以有任何修饰符(private,protected,public),可以在子类中被重新定义或赋值。 - 方法:
- Java 8 之前,接口中的方法默认是
public abstract,也就是只能有方法声明。 - 自 Java 8 起,可以在接口中定义
default方法和static方法。 - 自 Java 9 起,接口可以包含
private方法。 - 抽象类可以包含抽象方法和非抽象方法。
- 抽象方法没有方法体,必须在子类中实现。
- 非抽象方法有具体实现,可以直接在抽象类中使用或在子类中重写。
- Java 8 之前,接口中的方法默认是
在 Java 8 及以上版本中,接口引入了新的方法类型:default 方法、static 方法和 private 方法。这些方法让接口的使用更加灵活。
Java 8 引入的 default 方法用于提供接口方法的默认实现,可以在实现类中被覆盖。这样就可以在不修改实现类的情况下向现有接口添加新功能,从而增强接口的扩展性和向后兼容性。
public interface MyInterface {
default void defaultMethod() {
System.out.println("This is a default method.");
}
}Java 8 引入的 static 方法无法在实现类中被覆盖,只能通过接口名直接调用( MyInterface.staticMethod()),类似于类中的静态方法。static 方法通常用于定义一些通用的、与接口相关的工具方法,一般很少用。
public interface MyInterface {
static void staticMethod() {
System.out.println("This is a static method in the interface.");
}
}Java 9 允许在接口中使用 private 方法。private 方法可以用于在接口内部共享代码,不对外暴露。
public interface MyInterface {
// default 方法
default void defaultMethod() {
commonMethod();
}
// static 方法
static void staticMethod() {
commonMethod();
}
// 私有静态方法,可以被 static 和 default 方法调用
private static void commonMethod() {
System.out.println("This is a private method used internally.");
}
// 实例私有方法,只能被 default 方法调用。
private void instanceCommonMethod() {
System.out.println("This is a private instance method used internally.");
}
}String
String、StringBuffer 和 StringBuilder 的区别是什么?
可变性
简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以 String 对象是不可变的。
而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串 char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
在 Java 9 之后,
String、StringBuilder与StringBuffer的实现改用 byte 数组存储字符串private final byte[] value
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。
StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。
StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。
相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要多线程不安全的风险。
对于三者使用的总结:
- 操作少量的数据: 适用
String - 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder - 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
String 不可变:
- 可以使用字符串常量池
- 线程安全
- 加快字符串处理速度,hashcode 是定值。
String 为什么是不可变的?
String 类中使用 final 关键字修饰字符数组来保存字符串,所以 String 对象是不可变的。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
//...
}🐛 修正:我们知道被
final关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final关键字修饰的数组保存字符串并不是String不可变的根本原因,因为这个数组保存的字符串是可变的(final修饰引用类型变量的情况)。
String真正不可变有下面几点原因:
- 保存字符串的数组被
final修饰且为私有的,并且String类没有提供/暴露修改这个字符串的方法。String类被final修饰导致其不能被继承,进而避免了子类破坏String不可变。
字符串拼接使用+还是 StringBuilder?
在 Java 9 及以后的版本,非循环情况下,大胆使用
a + b + c + ...,参见 StringBuilder?来重温一下字符串拼接吧 open in new window
Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。
String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;上面的代码对应的字节码如下:

可以看出,字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象。
不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。
String[] arr = {"he", "llo", "world"};
String s = "";
for (int i = 0; i < arr.length; i++) {
s += arr[i];
}
System.out.println(s);StringBuilder 对象是在循环内部被创建的,这意味着每循环一次就会创建一个 StringBuilder 对象。

如果直接使用 StringBuilder 对象进行字符串拼接的话,就不会存在这个问题了。
String[] arr = {"he", "llo", "world"};
StringBuilder s = new StringBuilder();
for (String value : arr) {
s.append(value);
}
System.out.println(s);
如果你使用 IDEA 的话,IDEA 自带的代码检查机制也会提示你修改代码。
在 JDK 9 中,字符串相加“+”改为用动态方法 makeConcatWithConstants() 来实现,通过提前分配空间从而减少了部分临时对象的创建。然而这种优化主要针对简单的字符串拼接,如: a+b+c 。对于循环中的大量拼接操作,仍然会逐个动态分配内存(类似于两个两个 append 的概念),并不如手动使用 StringBuilder 来进行拼接效率高。这个改进是 JDK9 的 JEP 280open in new window 提出的,关于这部分改进的详细介绍,推荐阅读这篇文章:还在无脑用 StringBuilder?来重温一下字符串拼接吧 open in new window 以及参考 issue#2442open in new window。
String 的 intern 方法有什么作用?
String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
示例代码(JDK 1.8) :
// 在堆中创建字符串对象”Java“
// 将字符串对象”Java“的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); //trueObject
Object 类的常见方法总结
Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。
public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。
protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。
public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。
public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。
public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。
public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作相等判断
== 和 equals () 的区别
** == ** 对于基本类型和引用类型的作用效果是不同的:
- 对于基本数据类型来说,
==比较的是值。 - 对于引用数据类型来说,
==比较的是对象的内存地址。
因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
** equals() ** 作用不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals() 方法存在于 Object 类中,而 Object 类是所有类的直接或间接父类。Object 类 equals() 方法:
public boolean equals(Object obj) { return (this == obj); }String 中的 equals 方法是被重写过的,因为 Object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
System.out.println(aa == bb);// true
System.out.println(a == b);// false
System.out.println(a.equals(b));// true
System.out.println(42 == 42.0);// trueString 类 equals() 方法:
public boolean equals(Object anObject) {
if (this == anObject)
return true;
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
...
}hashCode () 介绍
hashCode() 的作用是获取哈希码( int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的,该方法通常用来将对象的内存地址转换为整数之后返回。
public native int hashCode();为什么要有 hashCode?
“ HashSet 如何检查重复”来说,每次都调用 equals() 方法来检查插入对象是否与内部对象相等太耗时间了,而检查 hashcode 值相对于 equals() 方法更快,从而提高执行速度。
下面这段内容摘自我的 Java 启蒙书《Head First Java》:
当你把对象加入
HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。。这样我们就大大减少了equals的次数,相应就大大提高了执行速度。
为什么重写 equals () 时必须重写 hashCode () 方法?
hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
简单来说就是:如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
HashSet、 HashMap 等对象,底层实现是先通过判断 hashCode 值是否相等,若相等再使用 equals() 进一步判断两个对象是否相等。如果没有重写 hashCode() 方法,虽然两个对象是相等的,但是 HashSet、 HashMap 等对象会认为两个对象不同,从而造成重复。
为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?
因为 hashCode() 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode )。
我们刚刚也提到了 HashSet, 如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。
泛型
Java 泛型了解么?什么是类型擦除?
Java 泛型(generics) 是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。引入泛型的原因是为了创建容器类。
Java 的泛型是伪泛型,这是因为 Java 在运行期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除。
List<Integer> list = new ArrayList<>();
list.add(12);
list.add("a");//这里直接添加会报错
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
//但是通过反射添加是可以的
//这就说明在运行期间所有的泛型信息都会被擦掉
add.invoke(list, "kl");
System.out.println(list);Java 泛型的好处
- 类型安全
- 泛型的主要目标是提高 Java 程序的类型安全
- 编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常
- 符合越早出错代价越小原则
- 消除强制类型转换
- 泛型的一个附带好处是,使用时直接得到目标类型,消除许多强制类型转换
- 所得即所需,这使得代码更加可读,并且减少了出错机会
- 潜在的性能收益
- 由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改
- 所有工作都在编译器中完成
- 编译器生成的代码跟不使用泛型(和强制类型转换)时所写的代码几乎一致,只是更能确保类型安全而已
什么是泛型中的限定通配符和非限定通配符 ?
限定通配符对类型进行了限制,有两种限定通配符
<? extends T>它通过确保类型必须是 T 的子类来设定类型的上界<? super T>它通过确保类型必须是 T 的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。
非限定通配符 ? , 可以用任意类型来替代。如 List<?> 的意思是这个集合是一个可以持有任意类型的集合,它可以是 List<A>,也可以是 List<B>, 或者 List<C> 等等。
可以把 List<String> 传递给一个接受 List<Object> 参数的方法吗?
不可以。真这样做的话会导致编译错误。因为 List 可以存储任何类型的对象包括 String, Integer 等等,而 List 却只能用来存储 String。
List<Object> objectList;
List<String> stringList;
objectList = stringList; //compilation error incompatible types判断 ArrayList<String> 与 ArrayList<Integer> 是否相等?
ArrayList<String> a = new ArrayList<String>();
ArrayList<Integer> b = new ArrayList<Integer>();
Class c1 = a.getClass();
Class c2 = b.getClass();
System.out.println(c1 == c2);输出的结果是 true。因为无论对于 ArrayList 还是 ArrayList,它们的 Class 类型都是一直的,都是 ArrayList. class。
那它们声明时指定的 String 和 Integer 到底体现在哪里呢?
答案是体现在类编译的时候。当 JVM 进行类编译时,会进行泛型检查,如果一个集合被声明为 String 类型,那么它往该集合存取数据的时候就会对数据进行判断,从而避免存入或取出错误的数据。
Java 泛型的使用
泛型一般有三种使用方式: 泛型类、泛型接口、泛型方法。
1. 泛型类:
//此处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; }
}2. 泛型接口 :
public interface Generator<T> {
public T method();
}实现泛型接口,不指定类型:
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() { return null; }
}实现泛型接口,指定类型:
class GeneratorImpl implements Generator<String>{
@Override
public String method() { return "hello"; }
}3. 泛型方法 :
public static <E> void printArray(E[] inputArray) {
for (E element : inputArray) { }
}使用:
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray(intArray);
printArray(stringArray);常用的通配符有哪些?
常用的通配符为: T,E,K,V,?
- ? 表示不确定的 Java 类型
- T (type) 表示具体的一个 Java 类型
- K V (key value) 分别代表 Java 键值中的 Key Value
- E (element) 代表 Element
你的项目中哪里用到了泛型?
- 可用于定义通用返回结果
CommonResult<T>通过参数T可根据具体的返回类型动态指定结果的数据类型 - 定义
Excel处理类ExcelUtil<T>用于动态指定Excel导出的数据类型 - 用于构建集合工具类。参考
Collections中的sort,binarySearch方法
反射
何为反射?
反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。
通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
反射机制优缺点
- 优点 : 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利
- 缺点 :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。Java Reflection: Why is it so slow?
反射的应用场景
像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。**这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。**比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 Method 来调用指定的方法。
public class DebugInvocationHandler implements InvocationHandler {
// 代理类中的真实对象
private final Object target;
public DebugInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
System.out.println("after method " + method.getName());
return result;
}
}
另外,像 Java 中的一大利器 注解 的实现也用到了反射。
为什么你使用 Spring 的时候,一个 @Component 注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value 注解就读取到配置文件中的值呢?这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。
获取 Class 对象的四种方式
如果我们动态获取到这些信息,我们需要依靠 Class 对象。Class 类对象将一个类的方法、变量等信息告诉运行的程序。Java 提供了四种方式获取 Class 对象:
1. 知道具体类的情况下可以使用:
Class alunbarClass = TargetObject.class;但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化。
2. 通过 Class.forName() 传入类的路径获取:
Class alunbarClass1 = Class.forName("cn.Javaguide.TargetObject");3. 通过对象实例 instance.getClass() 获取:
TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();4. 通过类加载器 xxxClassLoader.loadClass() 传入类路径获取:
Class clazz = ClassLoader.loadClass("cn.Javaguide.TargetObject");通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一些列步骤,静态块和静态对象不会得到执行
Java 反射 API 有几类?
反射 API 用来生成 JVM 中的类、接口或则对象的信息。
- Class 类:反射的核心类,可以获取类的属性,方法等信息。
- Field 类:Java. lang. reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。
- Method 类:Java. lang. reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行方法。
- Constructor 类:Java. lang. reflec 包中的类,表示类的构造方法。
反射的一些基本操作
- 创建一个我们要使用反射操作的类
TargetObject。
package cn.Javaguide;
public class TargetObject {
private String value;
public TargetObject() {}
public void publicMethod(String s) {
System.out.println("I love " + s);
}
private void privateMethod() {
System.out.println("value is " + value);
}
}- 使用反射操作这个类的方法以及参数
package cn.Javaguide;
import Java.lang.reflect.Field;
import Java.lang.reflect.InvocationTargetException;
import Java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException {
Class<?> tagetClass = Class.forName("cn.Javaguide.TargetObject");
TargetObject targetObject = (TargetObject) tagetClass.newInstance();
Method[] methods = tagetClass.getDeclaredMethods();
for (Method method : methods)
System.out.println(method.getName());
Method publicMethod = tagetClass.getDeclaredMethod("publicMethod", String.class);
publicMethod.invoke(targetObject, "JavaGuide");
Field field = tagetClass.getDeclaredField("value");
field.setAccessible(true); //跳过安全检查
field.set(targetObject, "JavaGuide");
Method privateMethod = tagetClass.getDeclaredMethod("privateMethod");
privateMethod.setAccessible(true);
privateMethod.invoke(targetObject);
}
}
反射机制的原理是什么?
Class actionClass=Class.forName(“MyClass”);
Object action=actionClass.newInstance();
Method method = actionClass.getMethod(“myMethod”,null);
method.invoke(action,null);上面就是最常见的反射使用的例子,前两行实现了类的装载、链接和初始化(newInstance 方法实际上也是使用反射调用了方法),后两行实现了从 class 对象中获取到 method 对象然后执行反射调用。
因反射原理较复杂,下面简要描述下流程,想要详细了解的小伙伴,可以看这篇文章:https://www.cnblogs.com/yougewe/p/10125073.html
- 反射获取类实例 Class. forName (),并没有将实现留给了 Java, 而是交给了 jvm 去加载!主要是先获取 ClassLoader, 然后调用 native 方法,获取信息,加载类则是回调 Java. lang. ClassLoader。最后,jvm 又会回调 ClassLoader 进类加载!
- newInstance () 主要做了三件事:
- 权限检测,如果不通过直接抛出异常;
- 查找无参构造器,并将其缓存起来;
- 调用具体方法的无参构造方法,生成实例并返回。
- 获取 Method 对象,
上面的 Class 对象是在加载类时由 JVM 构造的,JVM 为每个类管理一个独一无二的 Class 对象,这份 Class 对象里维护着该类的所有 Method,Field,Constructor 的 cache,这份 cache 也可以被称作根对象。
每次 getMethod 获取到的 Method 对象都持有对根对象的引用,因为一些重量级的 Method 的成员变量(主要是 MethodAccessor),我们不希望每次创建 Method 对象都要重新初始化,于是所有代表同一个方法的 Method 对象都共享着根对象的 MethodAccessor,每一次创建都会调用根对象的 copy 方法复制一份:
Method copy() {
Method res = new Method(clazz, name, parameterTypes, returnType,
exceptionTypes, modifiers, slot, signature,
annotations, parameterAnnotations, annotationDefault);
res.root = this;
res.methodAccessor = methodAccessor;
return res;
}- 调用 invoke () 方法。调用 invoke 方法的流程如下:
调用 Method. invoke 之后,会直接去调 MethodAccessor. invoke。MethodAccessor 就是上面提到的所有同名 method 共享的一个实例,由 ReflectionFactory 创建。
创建机制采用了一种名为 inflation 的方式(JDK1.4 之后):如果该方法的累计调用次数⇐15,会创建出 NativeMethodAccessorImpl,它的实现就是直接调用 native 方法实现反射;如果该方法的累计调用次数>15,会由 Java 代码创建出字节码组装而成的 MethodAccessorImpl。(是否采用 inflation 和 15 这个数字都可以在 jvm 参数中调整) 以调用 MyClass. myMethod (String s) 为例,生成出的 MethodAccessorImpl 字节码翻译成 Java 代码大致如下:
public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
public Object invoke(Object obj, Object[] args) throws Exception {
try {
MyClass target = (MyClass) obj;
String arg0 = (String) args[0];
target.myMethod(arg0);
} catch (Throwable t) {
throw new InvocationTargetException(t);
}
}
}异常
Java 异常类层次结构图

在 Java 中,所有的异常都有一个共同的祖先 Java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类 Exception (异常)和 Error (错误)。Exception 能被程序本身处理 (try-catch), Error 是无法处理的 (只能尽量避免)。
Exception 和 Error 二者都是 Java 异常处理的重要子类,各自都包含大量子类。
- **
Exception** : 程序本身可以处理的异常,可以通过catch来进行捕获。Exception又可以分为受检查异常 (必须处理) 和不受检查异常 (可以不处理)。 - **
Error** :Error属于程序无法处理的错误,我们没办法通过catch来进行捕获。例如,Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误 (OutOfMemoryError)、类定义错误(NoClassDefFoundError)等。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
受检查异常
Java 代码在编译过程中,如果受检查异常没有被 catch / throw 处理的话,就没办法通过编译。除了 RuntimeException 及其子类以外,其他的 Exception 类及其子类都属于受检查异常。常见的受检查异常有: IO 相关的异常、 ClassNotFoundException、 SQLException…。
不受检查异常
Java 代码在编译过程中,我们即使不处理不受检查异常也可以正常通过编译。
RuntimeException 及其子类都统称为非受检查异常,例如:NullPointerException、 NumberFormatException (字符串转换为数字)、 ArrayIndexOutOfBoundsException (数组越界)、 ClassCastException (类型转换错误)、 ArithmeticException (算术错误)等。
Throwable 类常用方法
- **
public String getMessage()**: 返回异常发生时的简要描述 - **
public String toString()**: 返回异常发生时的详细信息 - **
public String getLocalizedMessage()**: 返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同 - **
public void printStackTrace()**: 在控制台上打印Throwable对象封装的异常信息
throw 和 throws 区别
Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和抛出异常,可以通过 throws 关键字在方法上声明该方法要抛出的异常,或者在方法内部通过 throw 抛出异常对象。
throws 关键字和 throw 关键字在使用上的几点区别如下:
- throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。
- throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。
NoClassDefFoundError 和 ClassNotFoundException 区别?
NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致。
ClassNotFoundException 是一个受检查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class. forName, ClassLoader. loadClass 或 ClassLoader. findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常; 另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。
try-catch-finally
- **
try块:** 用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。 - **
catch块:** 用于处理 try 捕获到的异常。 - **
finally块:** 无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行。
在以下 3 种特殊情况下,finally 块不会被执行:
- 在
try或finally块中用了System.exit(int)退出程序。但是,如果System.exit(int)在异常语句之后,finally还是会被执行 - 程序所在的线程死亡。
- 关闭 CPU。
注意: 当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值。
catch 可以省略。
更为严格的说法其实是: try 只适合处理运行时异常,try+catch 适合处理运行时异常+普通异常。也就是说,如果你只用 try 去处理普通异常却不加以 catch 处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用 catch 显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以 catch 可以省略,你加上 catch 编译器也觉得无可厚非。
理论上,编译器看任何代码都不顺眼,都觉得可能有潜在的问题,所以你即使对所有代码加上 try,代码在运行期时也只不过是在正常运行的基础上加一层皮。但是你一旦对一段代码加上 try,就等于显示地承诺编译器,对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须用 catch 捕获以便进一步处理; 如果运行时异常,捕获然后丢弃并且+finally 扫尾处理,或者加上 catch 捕获以便进一步处理。
至于加上 finally,则是在不管有没捕获异常,都要进行的“扫尾”处理。
使用 try-with-resources 来代替 try-catch-finally
- 适用范围(资源的定义): 任何实现
Java.lang.AutoCloseable或者Java.io.Closeable的对象 - 关闭资源和 finally 块的执行顺序: 在
try-with-resources语句中,任何 catch 或 finally 块在声明的资源关闭后运行
《Effecitve Java》中明确指出:
面对必须要关闭的资源,我们总是应该优先使用
try-with-resources而不是try-finally。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources语句让我们更容易编写必须要关闭的资源的代码,若采用try-finally则几乎做不到这点。
Java 中类似于 InputStream、 OutputStream、 Scanner、 PrintWriter 等的资源都需要我们调用 close() 方法来手动关闭,一般情况下我们都是通过 try-catch-finally 语句来实现这个需求,如下:
Scanner scanner = null;
try {
scanner = new Scanner(new File("D://read.txt"));
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (scanner != null)
scanner.close();
}使用 Java 7 之后的 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-with-resources 块中声明多个资源。
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
int b;
while ((b = bin.read()) != -1)
bout.write(b);
catch (IOException e) {
e.printStackTrace();
}JVM 如何处理异常
在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。
注解
何谓注解?
Annotation (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
注解本质是一个继承了 Annotation 的特殊接口:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
public interface Override extends Annotation{
}JDK 提供了很多内置的注解(比如 @Override、@Deprecated),同时,我们还可以自定义注解。
注解的解析方法有哪几种?
注解只有被解析之后才会生效,常见的解析方法有两种:
- 编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用
@Override注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 - 运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的
@Value、@Component)都是通过反射来进行处理的。
代理模式
静态代理/动态代理的概念
静态代理是在编译时将接口、实现类、代理类转换为字节码文件,如 AspectJ。
动态代理时在运行时生成增强的代理类,如 Spring AOP。
静态代理的实现
- 定义一个接口及实现类
- 创建一个代理类实现该接口
- 将目标对象注入代理类中,在对应方法上调用该对象对应方法,同时可以在前后补充新逻辑。
动态代理的实现
动态代理主要包含两种形式:JDK 动态代理,CGLIB 动态代理。
JDK 动态代理:
- 定义一个被代理对象的接口及其实现类,如
SendMessService接口和SendMessServiceImpl实现类。 - 创建一个
InvocationHandler接口的实现类,并重写invoke()方法。在invoke()方法中我们会调用原生方法(被代理对象的方法)并自定义一些处理逻辑。 - 通过
Proxy.newProxyInstance()静态方法创建代理对象。可以采用工厂方法,将该函数放到工厂里。 - 使用时,直接操作代理对象即可。
CGLIB 动态代理:
- 定义一个类;
- 自定义
MethodInterceptor并重写intercept方法,intercept用于拦截增强被代理类的方法,和 JDK 动态代理中的invoke方法类似; - 通过
Enhancer类的create()创建代理类;
序列化
什么是序列化? 什么是反序列化?
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
简单来说:
- 序列化: 将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类 (Class),但是在 C++这种半面向对象的语言中,struct (结构体) 定义的是数据结构类型,而 class 对应的是对象类型。
序列化协议对应于 TCP/IP 4 层模型的哪一层?

OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。这不就对应的是序列化和反序列化么?
因为,OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。
序列化实现方式和区别
实现 Serializable 接口或者 Externalizable 接口。
Serializable 接口
类通过实现 Java.io.serializable 接口以启用其序列化功能。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。
Externalizable 接口
Externalizable 继承自 serializable,该接口中定义了两个抽象方法: writeExternal() 与 readExternal()。
当使用 Externalizable 接口来进行序列化与反序列化的时候需要开发人员重写 writeExternal() 与 readExternal() 方法。否则所有变量的值都会变成默认值。
区别
前者,系统自动存储必要的信息;只需要实现该接口,没有需要实现的方法;性能略差。
后者,程序决定存储哪些数据;需要实现读写方法;性能较高。
常见序列化协议有哪些?
JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。
像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。
serialVersionUID 作用、指定原因、修改时机
serialVersionUID 用来表明类的不同版本间的兼容性
Java 的序列化机制是通过在运行时判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体(类)的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。
为什么还要显示指定 serialVersionUID 的值?
如果不显示指定 serialVersionUID, JVM 在序列化时会根据属性自动生成一个 serialVersionUID, 然后与属性一起序列化, 再进行持久化或网络传输. 在反序列化时, JVM 会再根据属性自动生成一个新版 serialVersionUID, 然后将这个新版 serialVersionUID 与序列化时生成的旧版 serialVersionUID 进行比较, 如果相同则反序列化成功,否则报错.
如果显示指定了, JVM 在序列化和反序列化时仍然都会生成一个 serialVersionUID, 但值为我们显示指定的值, 这样在反序列化时新旧版本的 serialVersionUID 就一致了.
在实际开发中, 不显示指定 serialVersionUID 的情况会导致什么问题? 如果我们的类写完后不再修改, 那当然不会有问题, 但这在实际开发中是不可能的, 我们的类会不断迭代, 一旦类被修改了,那旧对象反序列化就会报错. 所以在实际开发中, 我们都会显示指定一个 serialVersionUID,值是多少无所谓, 只要不变就行。
什么时候修改 serialVersionUID
【强制】序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败; 如果完全不兼容升级,避免反序列化混乱,那么请修改 serialversionUID 值。注意 serialversionuID 不一致会抛出序列化运行时异常。
Java 序列化中如果有些字段不想进行序列化,怎么办?静态变量会被序列化吗?
对于不想进行序列化的变量,使用 transient 关键字修饰。
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
关于 transient 还有几点注意:
transient只能修饰变量,不能修饰类和方法。transient修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰int类型,那么反序列后结果就是0。static变量因为不属于任何对象 (Object),所以无论有没有transient关键字修饰,均不会被序列化。
看到这个结论, 是不是有人会问, serialVersionUID 也被 static 修饰, 为什么 serialVersionUID 会被序列化? 其实 serialVersionUID 属性并没有被序列化, JVM 在序列化对象时会自动生成一个 serialVersionUID, 然后将我们显示指定的 serialVersionUID 属性值赋给自动生成的 serialVersionUID。
Java I/O
获取用键盘输入常用的两种方法
方法 1:通过 Scanner
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();方法 2:通过 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: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
按操作方式分类结构图:

按操作对象分类结构图:

字符流转换为字节流
字节输入流转字符输入流通过 InputStreamReader 实现,该类的构造函数可以传入 InputStream 对象。
字节输出流转字符输出流通过 OutputStreamWriter 实现,该类的构造函数可以传入 OutputStream 对象。
字节流与字符流区别
- 一个读取字节,一个读取字符
- 字节流适合任何文件类型的读写,字符流适合纯文本数据。
- 内容无关选择字节流,内容有关选择字符流。
既然有了字节流, 为什么还要有字符流?
问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
Java IO 流的设计模式
适配器模式和装饰器模式。
适配器模式:
Reader reader = new InputStreamReader(inputstream);
把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作
装饰器模式:
new BufferedInputstream(new FileInputstream(inputstream));
一种动态地往一个类中添加新的行为的设计模式。就功能而言,装饰器模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。
BIO、NIO、AIO 的区别
BIO: 同步并阻塞,在服务器中实现的模式为一个连接一个线程。也就是说,客户端有连接请求的时候,服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然这也可以通过线程池机制改善。BIO 一般适用于连接数目小且固定的架构,这种方式对于服务器资源要求比较高,而且并发局限于应用中,是 JDK1.4 之前的唯一选择,但好在程序直观简单,易理解。
NIO: 同步并非阻塞,在服务器中实现的模式为一个请求一个线程,也就是说,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到有连接 IO 请求时才会启动一个线程进行处理。NIO 一般适用于连接数目多且连接比较短(轻操作)的架构,并发局限于应用中,编程比较复杂,从 JDK1.4 开始支持。
AIO: 异步并非阻塞,在服务器中实现的模式为一个有效请求一个线程,也就是说,客户端的 IO 请求都是通过操作系统先完成之后,再通知服务器应用去启动线程进行处理。AIO 一般适用于连接数目多且连接比较长(重操作)的架构,充分调用操作系统参与并发操作,编程比较复杂,从 JDK1.7 开始支持。
Java 中的 IO 模型
上一章所述 Unix 中的五种 I/O 模型,除信号驱动 I/O 外,Java 对其它四种 I/O 模型都有所支持。
- Java 传统 IO 模型即是同步阻塞 I/O
- NIO 是同步非阻塞 I/O
- 通过 NIO 实现的 Reactor 模式即是 I/O 多路复用模型的实现
- 通过 AIO 实现的 Proactor 模式即是异步 I/O 模型的实现
参考链接: 详解 Java 中 4 种 IO 模型 - Java 技术栈 - 博客园
学习 Netty
SPI
关于 SPI 的详细解读,请看这篇文章 Java SPI 机制详解 。
何谓 SPI?
SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。

SPI 和 API 有什么区别?
那 SPI 和 API 有啥区别?
说到 SPI 就不得不说一下 API(Application Programming Interface) 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下:

SPI VS API
一般模块之间都是通过接口进行通讯,因此我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。
- 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。
- 当接口存在于调用方这边时,这就是 **SPI ** 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。
举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。
SPI 的优缺点?
通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:
- 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
- 当多个
ServiceLoader同时load时,会有并发问题。