前言:此篇博客笔者参考了JavaGuide、三分恶等博主的八股文,结合Chat老师和自己的理解,整理了一篇关于Java基础的八股文。全篇图文并茂,每个知识点都有细致描述,详略得当,理解通透。希望对各位读者有所帮助,欢迎大家点赞、收藏、关注,后续将陆续推出后端八股文~~
什么是Java?
Java语言有哪些特点?
JVM、JDK和JRE有什么区别?
总之,JDK包含JRE,JRE包含JVM。
什么是Java的跨平台性?原理是什么?
什么是字节码? 采用字节码的好处是什么?
所谓的字节码,就是Java程序经过编译之后产生的.class文件,字节码能够被虚拟机识别,从而实现Java程序的跨平台性。
Java 程序从源代码到运行主要有三步:
只需要把Java程序编译成Java虚拟机能识别的Java字节码,不同的平台安装对应的Java虚拟机,这样就可以可以实现Java语言的平台无关性。
为什么说 Java 语言“编译与解释并存”?
高级编程语言按照程序的执行方式分为编译型和解释型两种。
Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为 Java 程序要经过先编译,后解释两个步骤。由 Java 编写的程序需要先经过编译步骤,生成字节码文件,这种字节码必须再经过JVM,解释成操作系统能识别的机器码,在由操作系统执行。因此,我们可以认为 Java 语言编译与解释并存。
Java有哪些数据类型?
需要注意一下,对于布尔类型:
自动类型转换、 强制类型转换? 看看这几行代码?
Java 所有的数值型变量可以相互转换,当把一个表数范围小的数值或变量直接赋给另一个表数范围大的变量时,可以进行自动类型转换;反之,需要强制转换。
这就好像,小杯里的水倒进大杯没问题,但大杯的水倒进小杯就不行了,可能会溢出。
float f = 3.4; // 对吗?
不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于向下转型,会造成精度损失。
正确写法:
float f = (float)3.4; // 写法1
float f = 3.4F; // 写法2
// 对吗?
short s1 = 1;
s1 = s1 + 1;
这会编译出错,因为1是int类型,而s1 + 1运算结果是int类型,要把这个结果赋值给左侧的short类型,这属于向下转型。向下转型并不是默认的,因此需要显示地强制类型转换:
short s1 = 1;
s1 = (short) (s1 + 1);
// 对吗?
short s1 = 1;
s1 += 1;
可以正确编译,因为s1 += 1
其实就相当于s1 = (short) (s1 + 1)
,这其中就有隐含的强制类型转换。
什么是自动拆箱/封箱?
Java可以自动对基本数据类型和它们的包装类进行装箱和拆箱。
举个栗子:
Integer i = 10; // 装箱
int n = i; // 拆箱
说说自增自减运算? 看下这几个代码运行结果?
++
和--
运算符可以放在变量之前,也可以放在变量之后。
例如,当 b = ++a
时,先自增(自己增加 1),再赋值(赋值给 b);当 b = a++
时,先赋值(赋值给 b),再自增(自己增加 1)。也就是,++a 输出的是 a+1 的值,a++输出的是 a 值。
看一下这段代码运行结果?
int i = 1;
i = i++;
System.out.println(i); // 输出结果是1
对于JVM而言,它对自增运算的处理,是会先定义一个临时变量来接收i
的值,然后进行自增运算,最后又将临时变量赋给了值为2的i
,所以最后的结果为1。
相当于这样的代码:
int i = 1;
int temp = i;
i++;
i = temp;
System.out.println(i);
这段代码会输出什么?
int count = 0;
for(int i = 0; i < 100; i++) {
count = count++;
}
System.out.println("count = " + count); // 输出结果是0
相当于这样的代码:
int count = 0;
int temp = count;
count++;
count = temp;
System.out.println(count);
面向对象和面向过程的区别?
面向对象有哪些特性?
面向对象的三大特性:封装、继承、多态。
将抽象出的数据、代码封装在一起,把⼀个对象的属性私有化,隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高代码复用性和安全性。
在已有类的基础上,通过增加新的属性或方法进而扩展形成新的类,提高代码复用性。继承是多态的前提。
关于继承有以下三个要点:
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
在 Java 中有两种形式可以实现多态:
或者这么理解,要实现多态需要做两件事:
重载(overload) 和重写(override) 的区别?
多态可以分为编译时多态(方法重载)和运行时多态(方法重写),方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同 或 参数个数不同 或 参数顺序不同)则视为重载。
重写是发生在子类与父类之间。
重写要求:
访问修饰符public、 private、 protected、 以及不写(默认) 时的 区别?
Java 支持 4 种不同的访问权限。
可见性 | private | default | protected | public |
---|---|---|---|---|
同一个类中 | √ | √ | √ | √ |
同一个包中 | × | √ | √ | √ |
子类中 | × | × | √ | √ |
全局范围 | × | × | × | √ |
抽象类(abstract class)和接口(interface)有什么区别?
相同: 接口和抽象类对实体类进行更高层次的抽象,仅定义公共行为和特征。
抽象类
抽象类的适用场景是当多个子类具有共同的方法实现时,可以将这些共同的方法实现放在抽象类中,由子类继承并扩展各自的方法,模板方法模式就是抽象类的一个典型应用。此外,抽象类也可以用来限制实例化,只有子类可以被实例化,从而增强程序的安全性。
接口
接口(Interface)是一种抽象类型,在 Java 8 之前,接口非常纯粹,只能包含抽象方法,也就是没有方法体的方法。而 Java 8 中接口出现了些许的变化,开始允许接口包含默认方法和静态方法。接口中的方法将自动被设置为 public 类型,属性/字段将被自动被设置为 public static final 类型。接口可以被实现(implements)多次,而一个类只能继承(extends)一个类。接口的主要作用是定义一组行为规范,让实现该接口的类能够符合规范并具有通用性。
区别 | 抽象类 | 接口 |
---|---|---|
成员变量 | 无特殊要求,可以和普通类─样定义任意类型 | 只能是 public static final 常量 |
构造方法 | 有构造方法,用于子类实例化使用 | 没有构造方法,不能实例化 |
方法 | 抽象类中可以做方法定义,也可以有方法实现。 有抽象方法的类一定是抽象类,在抽象类中,可以没有抽象方法。 | 接口只有定义,不能有方法的实现,JDK8 支持默认/静态方法,JDK9 支持私有方法 |
继承 | 单继承 | 多继承 |
成员变量与局部变量的区别有哪些?
静态变量和实例变量的区别? 静态方法、 实例方法呢?
静态变量和实例变量的区别:
静态方法和实例方法的区别:
在 JVM 类加载方面,实例方法和静态方法的区别主要体现在两个方面:
final关键字有什么作用?
final表示不可变的意思,可用于修饰类、属性和方法,但是不能修饰抽象类和接口(因为接口和抽象类本身就是用来继承或者实现的,与final的作用相斥)。
举个栗子说明第三条:
finale StringBuilder sb = new StringBuilder("abc");
sb.append("d");
System.out.println(sb); // 输出结果abcd
final、 finally、 finalize的区别?
try/catch
语句中,并且附带一个语句块表示这段语句最终一定被执行(无论是否抛出异常)。经常被用在需要释放资源的情况下,System.exit (0)
可以阻断 finally 执行。java.lang.Object
里定义的方法,也就是说每个对象都有这个方法,这个方法在GC启动,该对象被回收的时候进行调用。一个对象的 finalize 方法只会被调用一次,finalize 被调用并不一定会立即回收该对象。所以有可能调用 finalize 后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会再次调用 finalize 了,进而产生问题,因此不推荐使用 finalize 方法。==和 equals 的区别?
==
的作用是判断两个对象的地址是不是相等。即判断两个对象是不是同一个对象(基本数据类型==
比较的是值,引用数据类型==
比较的是内存地址)。也就是说:
equals的作用也是判断两个对象是否相等,注意它并不能用于比较基本数据类型哦。equals的两种使用情况:
equals()
比较该类的两个对象时,等价于通过 ==
比较这两个对象,还是相当于比较内存地址。equals()
方法来比较两个对象的内容而不是其引用。我们平时覆盖的equals()
方法一般是比较两个对象的内容是否相同,自定义了一个相等的标准,也就是两个对象的值是否相等。举个栗子说明类重写了equals方法:
// Person,我们认为两个人的编号和姓名相同,就是一个人:
public class Person {
private String no;
private String name;
@Override
public boolean equals(Object o) { // 重写equals方法
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return Objects.equals(no, person.no) && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(no, name);
}
}
hashCode()和equals()两种方法是什么关系?
如果两个对象相等,则hashcode一定也是相同的。理解:由于equals默认比较两个对象的地址是否相等,因此如果两个对象equals相等,则说明是同一个内存地址。而hashcode()方法这个方法通常用来将对象的内存地址转换为整数之后返回。故这两个对象得到的哈希码必然是相同的。
两个对象相等,对两个对象分别调用equals方法都返回true。反之,两个对象有相同的hashcode值,它们也不一定是相等的(因为在散列表中,hashCode() 相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等【哈希冲突】)。
为什么重写equals方法必须重写hashCode方法?
hashCode()的作用是获取哈希码,也称为散列码;它实际上是返回⼀个 int 整数,定义在 Object 类中,是一个本地方法,这个方法通常用来将对象的内存地址转换为整数之后返回。
public native int hashCode();
它们应遵守如下规定:
在 Java 中,如果一个类的实例需要被用作 Map 的键或者集合中的元素,就必须同时重写 hashCode() 和 equals() 方法。这是因为在 Map 和集合中,键和元素的比较是基于它们的 hashCode() 和 equals() 方法的结果。
举个栗子,当向 HashSet 中加入一个元素时,它需要判断集合中是否已经包含了这个元素,从而避免重复存储。
由于这个判断十分的频繁,所以要讲求效率,绝不能采用遍历集合逐个元素进行比较的方式。
HashSet 首先会调用对象的 hashCode() 方法获取其哈希码,并通过哈希码确定该对象在集合中存放的位置。假设这个位置之前已经存了一个对象,则 HashSet 会进一步调用 equals() 对两个对象进行比较:
而 Object 类提供的 equals() 方法默认是用 ==
来进行比较的,也就是说只有两个对象是同一个对象时(地址相同),才能返回相等的结果。而实际的业务中,我们通常的需求是,若两个不同的对象它们的内容是相同的,就认为它们相等。
鉴于这种情况,Object 类中 equals() 方法的默认实现是没有实用价值的,所以通常都要重写。 由于 hashCode() 与 equals() 具有联动关系,所以 equals() 方法重写时,通常也要将 hashCode() 进行重写,使得这两个方法始终满足上述规定。
Java是值传递, 还是引用传递?
结论:Java是值传递!
Java 程序设计语言总是采用「按值调用」。也就是说,方法得到的是所有参数值的一个拷贝,方法不能修改传递给它的任何参数变量的内容。
在 Java 中,方法参数共有两种类型:
Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。
JVM 的内存分为堆和栈,其中栈中存储了基本数据类型和引用数据类型实例的地址,也就是对象地址。而对象所占的空间是在堆中开辟的,所以传递的时候可以理解为把变量存储的对象地址给传递过去,因此引用类型也是值传递。
举个栗子,假定一个方法试图将一个参数值增加至 3 倍:
public static void tripleValue(double x){
x *= 3;
}
--------------------------
double percent = 10;
tripleValue(percent); // 调用这个方法之后,percent 的值还是 10
下面看一下具体的执行过程:
可以看到,一个方法不可能修改一个基本数据类型的参数。
但是对象引用作为参数就不同了,可以很容易地修改对象的字段值(比如我们有个 Employee
类,其中有字段 salary
和 方法 raiseSalary
):
class Employee {
private salary;
......
public void raiseSalary() {
salary += 200;
}
}
-----------------------
public static void tripleSalary (Employee x) {
x.raiseSalary();
}
-----------------------
harry = new Emplyee(...);
tipleSalary(harry);
具体的执行过程为:
harry
值的拷贝,这里是一个对象的引用。raiseSalary
方法应用于这个对象引用。x 和 harry
同时引用的那个 Employee
对象的薪金提高了 200。harry
继续引用那个薪金增加 200 的对象。深拷贝和浅拷贝的区别?
例如现在有一个order对象,里面有一个products列表,它的浅拷贝和深拷贝的示意图:
因此深拷贝是安全的,浅拷贝的话如果有引用类型,那么拷贝后对象,引用类型变量修改,会影响原对象。
浅拷贝如何实现呢?
Object类提供的clone()方法可以非常简单地实现对象的浅拷贝。
深拷贝如何实现呢?
- 重写克隆方法:重写克隆方法,引用类型变量单独克隆,这里可能会涉及多层递归。
- 序列化:可以先将原对象序列化,然后再反序列化成拷贝对象。
Java 创建对象有哪几种方式?
前两者都需要显式地调用构造方法。对于clone机制,需要注意浅拷贝和深拷贝的区别,对于序列化机制需要明确其实现原理,在Java中序列化可以通过实现Externalizable或者Serializable来完成。
String和StringBuilder、 StringBuffer的区别?
可变性 | 线程安全 | 性能 | |
---|---|---|---|
String | 不可变 | 因为不可变,所以是线程安全的 | 由于 String 是不可变的,所以每次对字符串进行修改时都会创建一个新的字符串对象,这样会产生很多临时对象,导致内存开销比较大 |
StringBuffer | 可变 | 线程安全的,因为其内部大多数方法都使用 synchronized 进行同步,不过其效率较低 |
StringBuffer 是可变的,在进行修改时不会创建新的对象,因此性能比 String 好;但是大量使用了同步方法,所以性能比 StringBuilder 差 |
StringBuilder | 可变 | 不是线程安全的,因为没有使用 synchronized 进行同步,这也是其效率高于 StringBuffer 的原因。单线程下,优先考虑使用 StringBuilder |
性能最好 |
可变和不可变:
是否线程安全:
性能:
String str1 = new String(“abc”)和String str2 = “abc” 和 区别?
两个语句都会去字符串常量池中检查是否已经存在“abc”,如果有则直接使用,如果没有则会在常量池中创建“abc” 对象。
但是不同的是,String str1 = new String(“abc”) 还会通过 new String() 在堆里创建一个"abc" 字符串对象实例。
new String(“hello”) 创建了几个字符串对象?
使用 new String(“hello”) 创建了两个字符串对象,其中一个是字符串常量池中的对象,另一个是堆内存中的对象。
当执行 new String(“hello”) 时:
因此,执行 new String(“hello”) 时,会创建两个不同的字符串对象:一个在字符串常量池中,一个在堆内存中。
但是,由于字符串常量池中已经存在字符串 “hello”,所以在堆内存中创建的对象其实是不必要的,这会造成额外的内存开销。因此,在实际开发中,一般不建议使用 String str = new String("hello")
这种方式来创建字符串对象,而是使用字符串字面值的方式来创建 String str = "hello"
。
String有哪些特性?
为什么说 String 是不可变的?
String
不可变的表现就是当我们试图对一个已有的对象 “abcd” 赋值为 “abcde”,String
会新创建一个对象:
String
用 final 修饰 char 数组,这个数组无法被修改:
但是!!!这个无法被修改仅仅是指引用地址不可被修改(也就是说栈里面的这个叫 value 的引用地址不可变,编译器不允许我们把 value 指向堆中的另一个地址),并不代表存储在堆中的这个数组本身的内容不可变。
举个栗子:
final int[] value = {1, 2, 3}
int[] newValue = {4, 5, ,6}
value = newValue; // 编译器报错,final不可变
如果我们直接修改数组中的元素,是完全 OK 的:
final int[] value = {1, 2, 3}
value[2] = 10; // 这时候数组里面已经是{1, 2, 10}
那既然我们说 String
是不可变的,那显然仅仅靠 final 是远远不够的:
String
类没有对外提供修改这个数组的方法,所以它初始化之后外界没有有效的手段去改变它;String
类被 final 修饰的,也就是不可继承,避免被他人继承后破坏;String
的所有方法里面,都很小心地避免去修改了 char 数组中的数据,涉及到对 char 数组中数据进行修改的操作全部都会重新创建一个 String
对象。你可以随便翻个源码看看来验证这个说法,比如 substring 方法:
String 为什么要设计成不可变的呢?
(1)首先,字符串常量池的需要。
如下面的代码所示,堆中只会创建一个 String
对象:
String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2) // true
假设 String
允许被改变,那如果我们修改了 str2 的内容为 good,那么 str1 也会被修改,显然这不是我们想要看见的结果。
(2)String
被设计成不可变就是为了安全。
作为最基础最常用的数据类型,String
被许多 Java 类库用来作为参数,如果 String
可变,将会引起各种安全隐患。
举个栗子,我们来看看将可变的字符串 StringBuilder
存入 HashSet
的场景:
我们把可变字符串 s3 指向了 s1 的地址,然后改变 s3 的值,由于 StringBuilder
没有像 String
那样设计成不可变的,所以 s3 就会直接在 s1 的地址上进行修改,导致 s1 的值也发生了改变。于是,糟糕的事情发生了,HashSet
中出现了两个相等的元素,破坏了 HashSet
的不包含重复元素的原则。
另外,在多线程环境下,众所周知,多个线程同时想要修改同一个资源,是存在危险的,而 String
作为不可变对象,不能被修改,并且多个线程同时读同一个资源,是完全没有问题的,所以 String
是线程安全的。
String 真的不可变吗?
想要改变 String
无非就是改变 char 数组 value 的内容,而 value 是私有属性,那么在 Java 中有没有某种手段可以访问类的私有属性呢?
没错,就是反射,使用反射可以直接修改 char 数组中的内容,当然,一般来说我们不这么做。
在使用 HashMap 的时候,用 String 做 key 有什么好处?
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因此要求key是不可变的。又由于String是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
两个字符串相加的底层是如何实现的?
intern方法有什么作用?
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
源码中也说明了:
字符串拼接的方式有哪些?
+
,底层用 StringBuilder 实现。只适用小数量,如果在循环中使用+
拼接,相当于不断创建新的 StringBuilder 对象再转换成 String 对象,效率极差。+
。说下String.hashCode()源码
String.hashCode()源码:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
从源码中可知:
String有一个私有变量hash来缓存哈希值,即当该串第一次调用hashCode()方法时,hash默认值为0,继续执行,当字符串长度大于0时计算出一个哈希值赋给hash,之后再调用hashCode()方法时不会重新计算,直接返回hash;
计算时,使用的是该字符串截成的一个字符数组,用每个字符的ASCII值进行计算,根据注释可以看出哈希计算公式是: s 0 × 3 1 n − 1 + s 1 × 3 1 n − 2 + ⋯ + s n − 1 s_0\times 31^{n-1} + s_1\times 31^{n-2} + \cdots + s_{n-1} s0×31n−1+s1×31n−2+⋯+sn−1。其中n是字符数组的长度,s是字符数组;
算法中还有一个乘数31,为什么使用31呢?
String类的equals()源码了解多少?
String类的equals源码:
public boolean equals(Object anObject) {
// 检查两个字符串是否指向同一个对象。如果是,则直接返回 true
if (this == anObject) {
return true;
}
// 检查给定的对象是否是 String 类型的。如果不是,则返回 false
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = count;
// 比较两个字符串的长度是否相等。如果不相等,则返回 false
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
// 逐个比较两个字符串的每个字符是否相等。如果有任意一个字符不相等,则返回 false;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
// 否则,返回 true
return true;
}
}
return false;
}
String源码中有哪些地方被final修饰?
java.lang.String 类的源码中,有以下几处地方被 final 修饰:
private final char[] value
:表示字符串的值的数组。这个字段被 final 修饰,意味着一旦初始化完成就不能再更改。private final int offset
:表示字符串的值数组的偏移量。这个字段被 final 修饰,意味着一旦初始化完成就不能再更改。private final int count
:表示字符串的值数组的长度。这个字段被 final 修饰,意味着一旦初始化完成就不能再更改。private final int hash
:表示字符串的哈希值。这个字段被 final 修饰,意味着一旦初始化完成就不能再更改。public final class String
:这个类被 final 修饰,意味着其不可以被继承。final int length()
方法就是一个返回字符串长度的方法,它被 final 修饰,意味着不能被子类覆盖。Integer a= 127,Integer b = 127;Integer c= 128,Integer d = 128;相等吗?
请观察以下代码:
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
Integer b1 = new Integer(127);
System.out.println(a == b); //true
System.out.println(b == b1); //false
Integer c = 128;
Integer d = 128;
System.out.println(c == d); //false
}
我们知道,==
拥有两种应用场景:
==
比较的是值内容是否相等;==
比较的是引用地址是否相等;这段代码主要涉及Java中的自动装箱和缓存机制。Java中的自动装箱是指将基本数据类型自动转换为对应的包装类对象。例如,int类型可以自动转换为Integer类型。当使用自动装箱时,Java会从一个内部的缓存池中获取包装对象,如果缓存池中已经存在该值的包装对象,那么直接返回该对象的引用。如果缓存池中不存在该值的包装对象,则创建新的对象并添加到缓存池中。
在Java中,自动装箱是指将基本类型转换为其对应的包装类类型。例如,将int转换为Integer。当我们使用自动装箱时,如果装箱的值在-128到127之间,Java会自动缓存这些值,以便进行重用。这意味着,如果我们在这个范围内创建两个相同的Integer对象,它们将引用相同的对象,因此在使用==
运算符进行比较时,它们将返回true。
在这段代码中,a和b都是在范围内的值,因此它们使用自动装箱得到的对象引用是相同的,因为在缓存池中已经存在值为127的Integer对象,所以a和b都指向同一个对象。而b和b1虽然包含相同的值,但是b是从缓存中获取的,而b1是通过显式创建一个新的Integer对象得到的,它们不是同一个对象,因此返回false。
当c和d的值为128时,c和d的值超出了缓存范围,它们使用自动装箱得到的对象引用是不同的,因为在缓存池中没有值为128的Integer对象,因此Java会创建两个不同的Integer对象。c和d引用的是不同的对象,因此返回false。
需要注意的是,在实际编程中,应该尽可能避免使用==
运算符来比较两个包装类对象,而应该使用equals()方法来比较它们的值。
包装类的缓存机制?
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。Byte
,Short
,Integer
,Long
这 4 种整数类型的包装类默认创建了数值[-128,127]的相应类型的缓存数据,Character
创建了数值在[0,127]范围的缓存数据,Boolean
直接返回 True
or False
。
IntegerCache
是 Integer
类中的静态内部类,综合这两段代码,我们大概也能知道,IntegerCache
其实就是个缓存,其中定义了一个缓冲区 cache
,用于存储 Integer
类型的数据,缓存区间是 [-128, 127]。
回到 valueOf
的源码:它首先会判断 int 类型的实参 i 是否在可缓存区间内,如果在,就直接从缓存 IntegerCache
中获取对应的 Integer
对象;如果不在缓存区间内,则会 new 一个新的 Integer
对象。
String转Integer的方法有哪些?
String转成Integer,主要有两个方法:
(1)Integer.parseInt(String s)
使用Integer类的静态方法parseInt()。该方法接受一个字符串参数,并返回该字符串表示的整数。
举个栗子:
String str = "123";
int i = Integer.parseInt(str);
(2)Integer.valueOf(String s)
使用Integer类的静态方法valueOf()。该方法接受一个字符串参数,并返回该字符串表示的整数的Integer包装类对象。
举个栗子:
String str = "123";
Integer i = Integer.valueOf(str);
不管哪一种,最终还是会调用Integer类内中的parseInt(String s, int radix)方法。
核心代码:
public static int parseInt(String s, int radix) throws NumberFormatException{
int result = 0;
// 是否是负数
boolean negative = false;
// char字符数组下标和长度
int i = 0, len = s.length();
……
int digit;
// 判断字符长度是否大于0,否则抛出异常
if (len > 0) {
……
while (i < len) {
// Accumulating negatively avoids surprises near MAX_VALUE
// 返回指定基数中字符表示的数值。(此处是十进制数值)
digit = Character.digit(s.charAt(i++),radix);
// 进制位乘以数值
result *= radix;
result -= digit;
}
}
// 根据上面得到的是否负数,返回相应的值
return negative ? result : -result;
}
注意:
Object 类的常见方法?
Object 类是一个特殊的类,是所有类的父类,也就是说所有类都可以调用它的方 法。它主要提供了以下 11 个方法,大概可以分为六类:
对象比较
public native int hashCode()
:native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。public boolean equals(Object obj)
:用于比较两个对象的内存地址是否相等,String 类对该方法进行了重写用户比较字符串的值是否相等。对象拷贝
protected native Object clone() throws CloneNotSupportedException
:native方法, 用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象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)。 所public final void wait() throws InterruptedException
:跟之前的两个wait方法一样, 只不过该方法一直等待,没有超时时间这个概念。反射
public final native Class> getClass()
:native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。垃圾回收
protected void finalize() throws Throwable
:通知垃圾收集器回收对象。介绍一下Java中的异常处理体系?
Java的异常体系是分为多层的。异常是指在程序运行期间出现的意外或错误情况。在 Java 中,所有的异常都是 Throwable 类或其子类的实例,Throwable 有两个直接子类:Error 和 Exception。
(1)Error
Error 表示应用程序无法处理的严重问题。通常情况下,Error 表示 JVM 自身出现的问题,比如 OutOfMemoryError
、StackOverflowError
等。Error一般是无法被程序员处理的,因为它们表示 JVM 中的严重问题,一旦发生就意味着程序无法继续执行。
(2)Exception
Exception 则表示应用程序本身出现了问题,可以通过修改代码进行避免或者处理。Exception 又分为两类:Checked Exception 和 Unchecked Exception(RuntimeException)。
try catch
捕获或者使用 throws 声明抛出,否则编译器会报错。这些异常通常是由外部环境或资源导致的,例如文件不存在、网络连接中断、数据库访问失败等。Checked Exception 包括 IOException
(IO 异常)、ClassNotFoundException
、SQLException
、NoSuchMethodException
、IllegalAccessException
、InterruptedException
等。常见的Checked Exception(编译时异常):
常见的Unchecked Exception(运行时异常):
ArithmeticException
:在算术操作中出现的异常,例如除数为零。NullPointerException
:在尝试访问空对象引用时出现的异常。ArrayIndexOutOfBoundsException
:在访问数组元素时越界时出现的异常。ClassCastException
:在进行类型转换时出现的异常。IllegalArgumentException
:在传递非法参数或参数值时出现的异常。IllegalStateException
:在对象状态不合法时出现的异常。UnsupportedOperationException
:在不支持操作时出现的异常。ConcurrentModificationException
:在并发修改集合时出现的异常。如何处理异常?
throw和throws
遇到异常不进行具体处理,而是继续抛给调用者 (throw,throws)。抛出异常有三种形式,一个是throw,另一个是throws,还有一种系统自动抛异常。throws用在方法上,后面跟的是异常类,可以跟多个;而throw用在方法内,后面跟的是异常对象。
try catch 捕获异常
三步走:
try
:将业务代码包裹在 try 块内部,当业务代码中发生任何异常时,系统都会为此异常创建一个异常对象。创建异常对象之后,JVM 会在 try 块之后寻找可以处理它的 catch 块,并将异常对象交给这个 catch 块处理。catch
:在 catch 块中处理异常时,应该先记录日志,便于以后追溯这个异常。然后根据异常的类型、结合当前的业务情况,进行相应的处理。比如,给变量赋予一个默认值、直接返回空值、向外抛出一个新的业务异常交给调用者处理,等等。finally
:如果业务代码打开了某个资源,比如数据库连接、网络连接、磁盘文件等,则需要在这段业务代码执行完毕后关闭这项资源。并且无论是否发生异常,都要尝试关闭这项资源。将关闭资源的代码写在 finally 块内,可以满足这种需求,即无论是否发生异常,finally 块内的代码总会被执行。举个栗子:
public class Example {
public static void main(String[] args) {
try {
// 包含可能会出现异常的代码以及声明异常的方法
myMethod();
} catch (MyException e) {
// 捕获异常并进行处理
e.printStackTrace();
} finally {
// 可选,必执行的代码
}
}
public static void myMethod() throws MyException {
// 抛出 MyException 异常
throw new MyException("This is a custom exception.");
}
}
如何自定义异常?
自定义异常一般都继承自 Exception
或者 RuntimeException
。其中
Exception
的异常被称为受检异常,需要在方法声明中显式抛出或者捕获处理RuntimeException
的异常则被称为非受检异常,可以不在方法声明中显式抛出或者捕获处理。自定义异常可以提供更加详细的异常信息,以便于调用者进行处理。一般来说,自定义异常应该包含以下内容:
以下是一个简单的自定义异常的示例:
public class MyException extends RuntimeException {
private String errorCode; // 异常状态码
private String errorMsg; // 异常状态信息
public MyException(String errorCode, String errorMsg) {
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getErrorMsg() {
return errorMsg;
}
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
}
使用自定义异常的示例代码如下:
public void myMethod() {
try {
// ...
} catch (Exception e) {
throw new MyException("10001", "发生异常了!");
}
}
在上面的示例代码中,如果 try
代码块中出现了异常,就会抛出自定义的 MyException
异常,并传递异常信息。调用者可以在捕获到 MyException
异常之后,根据异常信息来进行特殊处理。
在 finally 中执行 return 会发生什么 ?
在通常情况下,不要在 finally 块中使用 return、throw 等导致方法终止的语句,一旦在 finally 块中使用了 return、throw语句,将会导致try块、catch块中的return、throw语句失效。
当Java程序执行try块、catch块时遇到了return或throw语句,这两个语句都会导致该方法立即结束,但是系统在执行这两个语句前会先去寻找该异常处理流程中是否包含 finally 块:
如果 finally 块里也使用了 return 或 throw 等导致方法终止的语句,finally 块已经终止了方法,系统将不会跳回去执行 try 块、catch 块里的任何代码。
三道经典异常处理代码题
题目1
public class TryDemo {
public static void main(String[] args) {
System.out.println(test());
}
public static int test() {
try {
return 1;
} catch (Exception e) {
return 2;
} finally {
System.out.print("3");
}
}
}
// 执行结果:31
try、catch。finally 的基础用法,在 return 前会先执行 finally 语句块,所以是先输出 finally 里的 3,再输出 return 的 1。
题目2
public class TryDemo {
public static void main(String[] args) {
System.out.println(test1());
}
public static int test1() {
try {
return 2;
} finally {
return 3;
}
}
}
// 执行结果:3
try 返回前先执行 finally,结果 finally 里不按套路出牌,直接 return 了,自然也就走不到 try 里面的 return 了。
题目3
public class TryDemo {
public static void main(String[] args) {
System.out.println(test1());
}
public static int test1() {
int i = 0;
try {
i = 2;
return i;
} finally {
i = 3;
}
}
}
// 执行结果:2
大家可能会以为结果应该是 3,因为在 return 前会执行 finally,而 i 在 finally 中被修改为 3 了,那最终返回 i 不是应该为 3 吗?
但其实,在执行 finally 之前,JVM 会先将 i 的结果暂存起来,然后 finally 执行完毕后,会返回之前暂存的结果,而不是返回 i,所以即使 i 已经被修改为 3,最终返回的还是之前暂存起来的结果 2。
流按照不同的特点,有很多种划分方式。
Java I/O流共涉及40多个类,看上去杂乱,其实都存在一定的关联, Java I/O流的40多 个类都是从如下4个抽象类基类中派生出来的。
Java的IO流体系用到了一个设计模式——装饰器模式。
举个栗子,InputStream相关的部分类图如下:
说说什么是字节流和字符流?
Java中的I/O操作可以分为字节流和字符流两种类型,它们的主要区别在于处理的数据类型和编码方式不同。
介绍一下BIO、 NIO、 AIO?
BIO(blocking I/O)
就是传统的IO,同步阻塞,服务器中实现模式为一个连接一个线程。即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过连接池机制改善(实现多个客户连接服务器)。
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,程序简单易理解。
NIO
全称 java non-blocking IO,是指 JDK 提供的新 API。从JDK1.4开始,Java 提供了一系列改进的输入/输出的新特性,被统称为NIO(即New IO)。
NIO是同步非阻塞的,在服务器中实现的模式为一个请求一个线程,服务器端用一个线程处理多个连接,客户端发送的连接请求会注册到多路复用器上,多路复用器轮询到连接有IO请求就会进行处理:
NIO的数据是面向缓冲区Buffer的,必须从Buffer中读取或写入。
可以看出,NIO的运行机制:
NIO一般适用于连接数目多且连接比较短(轻操作)的架构,并发局限于应用中,编程比较复杂,从JDK1.4开始支持。
AIO
JDK 7 引入了 Asynchronous I/O,是异步不阻塞的 IO。异步并非阻塞,在服务器中实现的模式为一个有效请求一个线程,也就是说,客户端的IO请求都是通过操作系统先完成之后,再通知服务器应用去启动线程进行处理。
AIO一般适用于连接数目多且连接比较长(重操作)的架构,充分调用操作系统参与并发操作,编程比较复杂,从JDK1.7开始支持。
什么是序列化? 什么是反序列化?
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
类比我们生活中一些大件物品的运输,运输的时候把它拆了打包,用的时候再拆包组装。
若对象要支持序列化机制,则它的类需要实现 Serializable
接口,该接口是一个标记接口,它没有提供任何方法,只是标明该类是可以序列化的。Java 的很多类已经实现了Serializable接口,如包装类、 String、Date等。
若要实现序列化,则需要使用对象流 ObjectInputStream 和 ObjectOutputStream。其中,在序列化时需要调用 ObjectOutputStream 对象的 writeObject() 方法,以输出对象序列。在反序列化时需要调用 ObjectInputStream 对象的readObject() 方法,将对象序列恢复为对象。
如果有些变量不想序列化,怎么办?
对于不想进行序列化的变量,使用transient关键字修饰。
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。
说说有几种序列化方式?
什么是泛型?
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。泛型就是将类型参数化,其在编译时才确定具体的参数。
泛型的好处:
泛型类
// 此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于标识泛型
// 在实例化泛型类时,必须指定T的具体类型
public class Generic<T> {
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey() {
return this.key;
}
}
如何实例化泛型类:
Generic<Integer> genericInteger = new Generic<>(666);
泛型接口
class GeneratorImpl<T> implements Generator<T> {
@Override
public T method() {
return null;
}
}
实现泛型接口,指定类型:
class GeneratorImpl<T> implements Generator<String> {
@Override
public String method() {
return null;
}
}
泛型方法
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);
泛型常用的通配符有哪些?
常用的通配符为: T, E, K, V, ?
?
表示不确定的 java 类型T
(type) 表示具体的一个 java 类型E
(element) 代表 ElementK V
(key value) 分别代表 java 键值中的 Key Value什么是泛型擦除?
泛型擦除,官方名叫类型擦除(Type Erasure)。使用泛型的时候加上类型参数,编译器在编译的时候去掉类型参数。
声明了泛型的 .java
源代码,在编译生成 .class
文件之后,泛型相关的信息就消失了。可以认为,源代码中泛型相关的信息,就是提供给编译器用的。泛型信息对 Java 编译器可以见,对 Java 虚拟机不可见。泛型的实现原理就是类型擦除,泛型只存在于编译阶段,而不存在于运行阶段。编译后的字节码文件不包含泛型类型信息,因为虚拟机没有泛型类型对象,所有对象都属于普通类。
Java 编译器通过如下规则实现类型擦除:
Object
或者界定类型替代泛型。例如编译器会将 List
替换为 List
,泛型参数的具体类型信息被删除了,只留下了 Object 类型的信息。这样,编译器就可以将泛型类型当做普通的 Object 类型来处理了。之所以要有泛型擦除,主要是为了向下兼容,因为JDK5之前是没有泛型的,为了让JVM保持向下兼容,就出了泛型擦除这个策略。
谈谈你对注解的理解?
注解本质上是一个标记,注解可以标记在类上、方法上、属性上等,标记自身也可以设置一些值,有了标记之后,我们就可以在编译或者运行阶段去识别这些标记,然后做一些事情,这就是注解的用处。
注解是 JDK1.5 引入的特性,其实可以简单理解为 “标注”、“标签”。Java语言使用 @interface
语法来定义注解(Annotation
),它的格式如下:
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
注解的参数类似无参数方法,可以用 default
设定一个默认值
注解的本质是一个继承了 Annotation
的特殊接口:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
public interface Override extends Annotation{
}
什么是元注解?
有一些注解可以修饰其他注解,这些注解就称为元注解。简言之,元注解就是自定义注解的注解。
Java 标准库已经定义了一些元注解,我们只需要使用元注解,通常不需要自己去编写元注解。常见的元注解有四个:
@Target
@Retention
@Repeatable
@Inherited
(1)@Target
:定义Annotation
能够被应用于源码的哪些位置
ElementType.TYPE
ElementType.FIELD
ElementType.METHOD
ElementType.CONSTRUCTOR
ElementType.PARAMETER
举个栗子:
定义注解@Report
可用在方法上,我们必须添加一个@Target(ElementType.METHOD)
:
@Target(ElementType.METHOD)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
定义注解@Report
可用在方法或字段上,可以把@Target
注解参数变为数组{ ElementType.METHOD, ElementType.FIELD }
:
@Target({
ElementType.METHOD,
ElementType.FIELD
})
public @interface Report {
...
}
(2)@Retention
:定义了 Annotation
的生命周期
RetentionPolicy.SOURCE
(仅编译器):给编译器用的,不会写入 class 文件RetentionPolicy.CLASS
(仅class文件):会写入 class 文件,在类加载阶段丢弃,也就是运行的时候就没这个信息了RetentionPolicy.RUNTIME
(运行期):会写入 class 文件,永久保存,可以通过反射获取注解信息如果 @Retention
不存在,则该Annotation
默认为CLASS
。因为通常我们自定义的Annotation
都是RUNTIME
,所以,务必要加上@Retention(RetentionPolicy.RUNTIME)
这个元注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
(3)@Repeatable
:定义Annotation
是否可重复。这个注解应用不是特别广泛。
举个栗子:
@Repeatable(Reports.class)
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
@Target(ElementType.TYPE)
public @interface Reports {
Report[] value();
}
经过@Repeatable
修饰后,在某个类型声明处,就可以添加多个@Report
注解:
@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}
(4)@Inherited
:定义子类是否可继承父类定义的Annotation
。
@Inherited
仅针对@Target(ElementType.TYPE)
类型的annotation
有效,并且仅针对class
的继承,对interface
的继承无效:
举个栗子:
@Inherited
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
在使用的时候,如果一个类用到了@Report
:
@Report(type=1)
public class Person {
}
则它的子类默认也定义了该注解:
public class Student extends Person {
}
注解的实现原理?
注解的本质是继承了 Annotation
的特殊接口,注解中定义的注解成员属性会转化为抽象方法,那么最后这些注解成员属性怎么进行赋值的呢?
答案就是:为注解对应的接口生成一个实现该接口的动态代理类。
Java 通过 JDK 动态代理的方式生成了一个实现了"注解对应接口"的实例,该代理类实例实现了"注解成员属性对应的方法",这个步骤类似于"注解成员属性"的赋值过程,这样子就可以在程序运行的时候通过反射获取到注解的成员属性(这里注解必须是运行时可见的,也就是使用了@Retention(RetentionPolicy.RUNTIME
)。
什么是反射?
我们通常都是利用 new 方式来创建对象实例,这可以说就是一种“正射”,这种方式在编译时候就确定了类型信息。而如果我们想动态地获取类信息、创建类实例、调用类方法这时候就要用到反射。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
反射:在运行状态中,对于任意一个类都能知道它的所有属性和方法,对于任意一个对象都能调用它的任意方法和属性,这种动态获取信息及调用对象方法的功能称为反射。
反射的作用?
反射拥有以下四大功能:
这种动态获取信息、动态创建/调用对象的方法的功能就称为 Java 语言的反射机制。
反射的原理?
在通常情况下,一定是先有类然后再 new 一个对象出来的对吧,类的正常加载过程是这样的:
Date date = new Date(); // 后有对象
首先 JVM 会将我们的代码编译成一个 .class
字节码文件,然后被类加载器(ClassLoader)加载进 JVM 的内存中,同时会创建这个类的 Class
对象存到堆中(注意这个不是 new 出来的对象,而是类的类型对象)。JVM 在实例化这个类的对象 date 前,会先检查其类(Date)是否加载,寻找类对应的 Class
对象,若加载好,则为其分配内存,然后再进行初始化 new
操作。
那么在加载完一个类后,堆内存的方法区就产生了一个 Class
对象,并且包含了这个类的完整结构信息,我们可以通过这个 Class
对象看到类的结构,就好比一面镜子。所以我们形象的称之为:反射
说的再详细点,在通常情况下,一定是先有类再有对象,我们把这个通常情况称为 “正”。那么反射中的这个 “反” 我们就可以理解为根据对象找到对象所属的类(对象的出处)。
Date date = new Date();
System.out.println(date.getClass()); // "class java.util.Date"
通过反射,也就是调用了 getClass()
方法后,我们就获得了这个类对应的 Class
对象,看到了这个类的结构,输出了类对象所属的类的完整名称,即找到了对象的出处。当然,获取 Class
对象的方式不止这一种。
简言之,反射的原理就是通过将类对应的字节码文件加载到JVM内存中得到一个Class对象,然后通过这个Class对象可以反向获取实例的各个属性以及调用它的方法。
获取 Class 类对象有几种方式?
从 Class
类的源码可以看出,它的构造函数是私有的,也就是说只有 JVM 可以创建 Class
类的对象,我们不能像普通类一样直接 new 一个 Class
对象。
我们只能通过已有的类来得到一个 Class
类对象,Java 提供了四种方式:
第一种:知道具体类的情况下可以使用
Class alunbarClass = TargetObject.class;
但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化。
第二种:通过 Class.forName()
传入全类名获取
Class alunbarClass1 = Class.forName("com.xxx.TargetObject");
第 2 个 boolean
参数表示类是否需要初始化,默认是需要初始化。一旦初始化,就会触发目标对象的 static
块代码执行,static
参数也会被再次初始化。
第三种:通过对象实例 instance.getClass()
获取
Date date = new Date();
Class alunbarClass2 = date.getClass(); // 获取该对象实例的 Class 类对象
第四种:通过类加载器 xxxClassLoader.loadClass()
传入类路径获取
class clazz = ClassLoader.LoadClass("com.xxx.TargetObject");
通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一些列步骤,静态块和静态对象不会得到执行。
JDK1.8有哪些新特性?
举个栗子
Lambda表达式:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.forEach(x -> System.out.println(x));
函数式接口(Functional Interface)和方法引用:
@FunctionalInterface
interface Converter<F, T> {
T convert(F from);
}
Converter<String, Integer> converter = Integer::valueOf;
Integer result = converter.convert("123);
Stream API:
List<String> fruits = Arrays.asList("Apple", "Banana", "Orange");
fruits.stream()
.filter(fruit -> fruit.startsWith("A"))
.map(String::toUpperCase)
.forEach(System.out::println);
接口默认方法(Default Methods):
interface Vehicle {
void start();
default void stop() {
System.out.println("Vehicle stopped");
}
}
class Car implements Vehicle {
@Override
public void start() {
System.out.println("Car started");
}
}
Car car = new Car();
car.start();
car.stop();
方法引用和构造函数引用:
List<String> names = Arrays.asList("John", "Alice", "Bob");
names.sort(String::compareToIgnoreCase);
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> list = listSupplier.get();
新的日期和时间 API(java.time 包):
LocalDate today = LocalDate.now();
LocalTime now = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.of(today, now);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDateTime = dateTime.format(formatter);
System.out.println(formattedDateTime);
Java8有哪些内置函数式接口?
JDK 1.8 API 包含了很多内置的函数式接口。其中就包括我们在老版本中经常见到的 Comparator 和 Runnable,Java 8 为他们都添加了 @FunctionalInterface 注解,以用来支持 Lambda 表达式。除了这两个之外,还有Callable、Predicate、Function、Supplier、Consumer等等。
Optional了解吗?
Optional 是用于防范 NullPointerException 。
可以将 Optional 看做是包装对象(可能是 null , 也有可能非 null )的容器。 当我们定义了 一个方法,这个方法返回的对象可能是空,也有可能非空的时候,我们就可以考虑用 Optional 来包装它,这也是在 Java 8 被推荐使用的做法。
Optional<String> optional = Optional.of("bam");
optional.isPresent(); // true
optional.get(); // "bam"
optional.orElse("fallback"); // "bam"
optional.ifPresent((s) -> System.out.println(s.charAt(0)));
API:
List<String> fruits = Arrays.asList("Apple", "Banana", "Orange");
fruits.stream()
.filter(fruit -> fruit.startsWith("A"))
.map(String::toUpperCase)
.forEach(System.out::println);
接口默认方法(Default Methods):
interface Vehicle {
void start();
default void stop() {
System.out.println("Vehicle stopped");
}
}
class Car implements Vehicle {
@Override
public void start() {
System.out.println("Car started");
}
}
Car car = new Car();
car.start();
car.stop();
方法引用和构造函数引用:
List<String> names = Arrays.asList("John", "Alice", "Bob");
names.sort(String::compareToIgnoreCase);
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> list = listSupplier.get();
新的日期和时间 API(java.time 包):
LocalDate today = LocalDate.now();
LocalTime now = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.of(today, now);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDateTime = dateTime.format(formatter);
System.out.println(formattedDateTime);
Java8有哪些内置函数式接口?
JDK 1.8 API 包含了很多内置的函数式接口。其中就包括我们在老版本中经常见到的 Comparator 和 Runnable,Java 8 为他们都添加了 @FunctionalInterface 注解,以用来支持 Lambda 表达式。除了这两个之外,还有Callable、Predicate、Function、Supplier、Consumer等等。
Optional了解吗?
Optional 是用于防范 NullPointerException 。
可以将 Optional 看做是包装对象(可能是 null , 也有可能非 null )的容器。 当我们定义了 一个方法,这个方法返回的对象可能是空,也有可能非空的时候,我们就可以考虑用 Optional 来包装它,这也是在 Java 8 被推荐使用的做法。
Optional<String> optional = Optional.of("bam");
optional.isPresent(); // true
optional.get(); // "bam"
optional.orElse("fallback"); // "bam"
optional.ifPresent((s) -> System.out.println(s.charAt(0)));