diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 4e0dc7d8..5394bf3a 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -167,7 +167,7 @@ module.exports = config({ { title: "基础", prefix: "basis/", children: [ - "java基础知识总结", + "java-basic-questions-01", "java-basic-questions-02", "java-basic-questions-03", { title: "重要知识点", children: [ diff --git a/docs/java/basis/java基础知识总结.md b/docs/java/basis/java-basic-questions-01.md similarity index 52% rename from docs/java/basis/java基础知识总结.md rename to docs/java/basis/java-basic-questions-01.md index a268f283..1d9793cc 100644 --- a/docs/java/basis/java基础知识总结.md +++ b/docs/java/basis/java-basic-questions-01.md @@ -1,5 +1,5 @@ --- -title: Java基础知识&面试题总结 +title: Java基础知识&面试题总结(上) category: Java tag: - Java基础 @@ -50,7 +50,7 @@ JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有 **Java 程序从源代码到运行的过程如下图所示:** -![Java程序转变为机器代码的过程](./images/java程序转变为机器代码的过程.png) +![Java程序转变为机器代码的过程](/Users/guide/Documents/GitHub/JavaGuide/docs/java/basis/images/java程序转变为机器代码的过程.png) 我们需要格外注意的是 `.class->机器码` 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(just-in-time compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 **Java 是编译与解释共存的语言** 。 @@ -127,7 +127,7 @@ JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有 > java 编程思想第四版:2.2.2 节 > ![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-15/86735519.jpg) -### 可变长参数 +### 使用过可变长参数吗? 从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面的这个 `printVariable` 方法就可以接受 0 个或者多个参数。 @@ -206,7 +206,7 @@ public class VariableLengthArgument { } ``` -### 注释 +### 注释有哪几种?注释越多越好吗? Java 中的注释有三种: @@ -437,119 +437,6 @@ public class SuperSuperMan extends SuperMan { } ``` -### 泛型 - -#### Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符? - -**Java 泛型(generics)** 是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。 - -Java 的泛型是伪泛型,这是因为 Java 在运行期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。 - -```java -List list = new ArrayList<>(); - -list.add(12); -//这里直接添加会报错 -list.add("a"); -Class clazz = list.getClass(); -Method add = clazz.getDeclaredMethod("add", Object.class); -//但是通过反射添加是可以的 -//这就说明在运行期间所有的泛型信息都会被擦掉 -add.invoke(list, "kl"); -System.out.println(list); -``` - -泛型一般有三种使用方式: 泛型类、泛型接口、泛型方法。 - -**1.泛型类**: - -```java -//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 -//在实例化泛型类时,必须指定T的具体类型 -public class Generic { - private T key; - public Generic(T key) { - this.key = key; - } - public T getKey() { - return key; - } -} -``` - -如何实例化泛型类: - -```java -Generic genericInteger = new Generic(123456); -``` - -**2.泛型接口** : - -```java -public interface Generator { - public T method(); -} -``` - -实现泛型接口,不指定类型: - -```java -class GeneratorImpl implements Generator{ - @Override - public T method() { - return null; - } -} -``` - -实现泛型接口,指定类型: - -```java -class GeneratorImpl implements Generator{ - @Override - public String method() { - return "hello"; - } -} -``` - -**3.泛型方法** : - -```java -public static void printArray(E[] inputArray) { - for (E element : inputArray) { - System.out.printf("%s ", element); - } - System.out.println(); -} -``` - -使用: - -```java -// 创建不同类型数组: Integer, Double 和 Character -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` 可根据具体的返回类型动态指定结果的数据类型 -- 定义 `Excel` 处理类 `ExcelUtil` 用于动态指定 `Excel` 导出的数据类型 -- 用于构建集合工具类。参考 `Collections` 中的 `sort`, `binarySearch` 方法 -- ...... - ### == 和 equals() 的区别 **`==`** 对于基本类型和引用类型的作用效果是不同的: @@ -684,7 +571,7 @@ public native int hashCode(); ## 基本数据类型 -### Java 中的几种基本数据类型是什么?各自占用多少字节呢?对应的包装类型是什么? +### Java 中的几种基本数据类型了解么? Java 中有 8 种基本数据类型,分别为: @@ -874,620 +761,6 @@ private static long sum() { } ``` -## Java 面向对象 - -### 面向对象和面向过程的区别 - -- **面向过程** :**面向过程性能比面向对象高。** 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,**面向过程没有面向对象易维护、易复用、易扩展。** -- **面向对象** :**面向对象易维护、易复用、易扩展。** 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,**面向对象性能比面向过程低**。 - -参见 issue : [面向过程 :面向过程性能比面向对象高??](https://github.com/Snailclimb/JavaGuide/issues/431) - -> 这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java 性能差的主要原因并不是因为它是面向对象语言,而是 Java 是半编译语言,最终的执行代码并不是可以直接被 CPU 执行的二进制机械码。 -> -> 而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比 Java 好。 - -### 成员变量与局部变量的区别有哪些? - -1. 从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 `public`,`private`,`static` 等修饰符所修饰,而局部变量不能被访问控制修饰符及 `static` 所修饰;但是,成员变量和局部变量都能被 `final` 所修饰。 -2. 从变量在内存中的存储方式来看,如果成员变量是使用 `static` 修饰的,那么这个成员变量是属于类的,如果没有使用 `static` 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 -3. 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。 -4. 从变量是否有默认值来看,成员变量如果没有被赋初,则会自动以类型的默认值而赋值(一种情况例外:被 `final` 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。 - -### 创建一个对象用什么运算符?对象实体与对象引用有何不同? - -new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。 - -一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。 - -### 对象的相等与指向他们的引用相等,两者有什么不同? - -对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。 - -### 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么? - -构造方法主要作用是完成对类对象的初始化工作。 - -如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了,这时候,就不能直接 new 一个对象而不传递参数了,所以我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑。 - -### 构造方法有哪些特点?是否可被 override? - -特点: - -1. 名字与类名相同。 -2. 没有返回值,但不能用 void 声明构造函数。 -3. 生成类的对象时自动执行,无需调用。 - -构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。 - -### 面向对象三大特征 - -#### 封装 - -封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。就好像我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。就好像如果没有空调遥控器,那么我们就无法操控空凋制冷,空调本身就没有意义了(当然现在还有很多其他方法 ,这里只是为了举例子)。 - -```java -public class Student { - private int id;//id属性私有化 - private String name;//name属性私有化 - - //获取id的方法 - public int getId() { - return id; - } - - //设置id的方法 - public void setId(int id) { - this.id = id; - } - - //获取name的方法 - public String getName() { - return name; - } - - //设置name的方法 - public void setName(String name) { - this.name = name; - } -} -``` - -#### 继承 - -不同类型的对象,相互之间经常有一定数量的共同点。例如,小明同学、小红同学、小李同学,都共享学生的特性(班级、学号等)。同时,每一个对象还定义了额外的特性使得他们与众不同。例如小明的数学比较好,小红的性格惹人喜爱;小李的力气比较大。继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。 - -**关于继承如下 3 点请记住:** - -1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,**只是拥有**。 -2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 -3. 子类可以用自己的方式实现父类的方法。(以后介绍)。 - -#### 多态 - -多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。 - -**多态的特点:** - -- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系; -- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定; -- 多态不能调用“只在子类存在但在父类不存在”的方法; -- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。 - -### String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的? - -**可变性** - -简单的来说:`String` 类中使用 `final` 关键字修饰字符数组来保存字符串,~~所以`String` 对象是不可变的。~~ - -```java -public final class String implements java.io.Serializable, Comparable, CharSequence { - private final char value[]; - //... -} -``` - -> 🐛 修正 : 我们知道被 `final` 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,`final` 关键字修饰的数组保存字符串并不是 `String` 不可变的根本原因,因为这个数组保存的字符串是可变的(`final` 修饰引用类型变量的情况)。 -> -> `String` 真正不可变有下面几点原因: -> -> 1. 保存字符串的数组被 `final` 修饰且为私有的,并且`String` 类没有提供/暴露修改这个字符串的方法。 -> 2. `String` 类被 `final` 修饰导致其不能被继承,进而避免了子类破坏 `String` 不可变。 -> -> 相关阅读:[如何理解 String 类型值的不可变? - 知乎提问](https://www.zhihu.com/question/20618891/answer/114125846) -> -> 补充(来自[issue 675](https://github.com/Snailclimb/JavaGuide/issues/675)):在 Java 9 之后,`String` 、`StringBuilder` 与 `StringBuffer` 的实现改用 byte 数组存储字符串。 - -`StringBuilder` 与 `StringBuffer` 都继承自 `AbstractStringBuilder` 类,在 `AbstractStringBuilder` 中也是使用字符数组保存字符串,不过没有使用 `final` 和 `private` 关键字修饰,最关键的是这个 `AbstractStringBuilder` 类还提供了很多修改字符串的方法比如 `append` 方法。 - -```java -abstract class AbstractStringBuilder implements Appendable, CharSequence { - char[] value; - public AbstractStringBuilder append(String str) { - if (str == null) - return appendNull(); - int len = str.length(); - ensureCapacityInternal(count + len); - str.getChars(0, len, value, count); - count += len; - return this; - } - //... -} -``` - -**线程安全性** - -`String` 中的对象是不可变的,也就可以理解为常量,线程安全。`AbstractStringBuilder` 是 `StringBuilder` 与 `StringBuffer` 的公共父类,定义了一些字符串的基本操作,如 `expandCapacity`、`append`、`insert`、`indexOf` 等公共方法。`StringBuffer` 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。`StringBuilder` 并没有对方法进行加同步锁,所以是非线程安全的。 - -**性能** - -每次对 `String` 类型进行改变的时候,都会生成一个新的 `String` 对象,然后将指针指向新的 `String` 对象。`StringBuffer` 每次都会对 `StringBuffer` 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 `StringBuilder` 相比使用 `StringBuffer` 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 - -**对于三者使用的总结:** - -1. 操作少量的数据: 适用 `String` -2. 单线程操作字符串缓冲区下操作大量数据: 适用 `StringBuilder` -3. 多线程操作字符串缓冲区下操作大量数据: 适用 `StringBuffer` - -### Object 类的常见方法总结 - -Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法: - -```java -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 { }//实例被垃圾回收器回收的时候触发的操作 -``` - -### 深拷贝和浅拷贝区别了解吗?什么是引用拷贝? - -关于深拷贝和浅拷贝区别,我这里先给结论: - -- **浅拷贝**:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。 -- **深拷贝** :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。 - -上面的结论没有完全理解的话也没关系,我们来看一个具体的案例! - -**浅拷贝** - -浅拷贝的示例代码如下,我们这里实现了 `Cloneable` 接口,并重写了 `clone()` 方法。 - -`clone()` 方法的实现很简单,直接调用的是父类 `Object` 的 `clone()` 方法。 - -```java -public class Address implements Cloneable{ - private final String name; - // 省略构造函数、Getter&Setter方法 - @Override - public Address clone() { - try { - return (Address) super.clone(); - } catch (CloneNotSupportedException e) { - throw new AssertionError(); - } - } -} - -public class Person implements Cloneable { - private Address address; - // 省略构造函数、Getter&Setter方法 - @Override - public Person clone() { - try { - Person person = (Person) super.clone(); - return person; - } catch (CloneNotSupportedException e) { - throw new AssertionError(); - } - } -} -``` - -测试 : - -```java -Person person1 = new Person(new Address("武汉")); -Person person1Copy = person1.clone(); -// true -System.out.println(person1.getAddress() == person1Copy.getAddress()); -``` - -从输出结构就可以看出, `person1` 的克隆对象和 `person1` 使用的仍然是同一个 `Address` 对象。 - -**深拷贝** - -这里我们简单对 `Person` 类的 `clone()` 方法进行修改,连带着要把 `Person` 对象内部的 `Address` 对象一起复制。 - -```java -@Override -public Person clone() { - try { - Person person = (Person) super.clone(); - person.setAddress(person.getAddress().clone()); - return person; - } catch (CloneNotSupportedException e) { - throw new AssertionError(); - } -} -``` - -测试 : - -```java -Person person1 = new Person(new Address("武汉")); -Person person1Copy = person1.clone(); -// false -System.out.println(person1.getAddress() == person1Copy.getAddress()); -``` - -从输出结构就可以看出,虽然 `person1` 的克隆对象和 `person1` 包含的 `Address` 对象已经是不同的了。 - -**那什么是引用拷贝呢?** 简单来说,引用拷贝就是两个不同的引用指向同一个对象。 - -我专门画了一张图来描述浅拷贝、深拷贝、引用拷贝: - -![](./images/shallow&deep-copy.png) - -## 反射 - -### 何为反射? - -如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。 - -反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。 - -通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。 - -### 反射机制优缺点 - -- **优点** : 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利 -- **缺点** :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。[Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow) - -### 反射的应用场景 - -像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。 - -但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。 - -**这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。** - -比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 `Method` 来调用指定的方法。 - -```java -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`注解就读取到配置文件中的值呢?究竟是怎么起作用的呢? - -这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。 - -## 注解 - -`Annontation` (注解) 是Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量。 - -注解本质是一个继承了`Annotation` 的特殊接口: - -```java -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.SOURCE) -public @interface Override { - -} - -public interface Override extends Annotation{ - -} -``` - -注解只有被解析之后才会生效,常见的解析方法有两种: - -- **编译期直接扫描** :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用`@Override` 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 -- **运行期通过反射处理** :像框架中自带的注解(比如 Spring 框架的 `@Value` 、`@Component`)都是通过反射来进行处理的。 - -JDK 提供了很多内置的注解(比如 `@Override` 、`@Deprecated`),同时,我们还可以自定义注解。 - -## 异常 - -**Java 异常类层次结构图概览** : - -![types-of-exceptions-in-java](/Users/guide/Documents/GitHub/JavaGuide/docs/java/basis/images/types-of-exceptions-in-java.png) - - - -![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-12/Java%E5%BC%82%E5%B8%B8%E7%B1%BB%E5%B1%82%E6%AC%A1%E7%BB%93%E6%9E%84%E5%9B%BE2.png) - -

图片来自:https://chercher.tech/java-programming/exceptions-java

- -### Exception 和 Error 有什么区别? - -在 Java 中,所有的异常都有一个共同的祖先 `java.lang` 包中的 `Throwable` 类。`Throwable` 类有两个重要的子类: - -- **`Exception`** :程序本身可以处理的异常,可以通过 `catch` 来进行捕获。`Exception` 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。 -- **`Error`** :`Error` 属于程序无法处理的错误 ,我们没办法通过 `catch` 来进行捕获 。例如Java 虚拟机运行错误(`Virtual MachineError`)、虚拟机内存不够错误(`OutOfMemoryError`)、类定义错误(`NoClassDefFoundError`)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。 - -### Checked Exception 和 Unchecked Exception 有什么区别? - -**Checked Exception** 即受检查异常,Java 代码在编译过程中,如果受检查异常没有被 `catch`/`throw` 处理的话,就没办法通过编译 。 - -比如下面这段 IO 操作的代码: - -![](./images/checked-exception.png) - -除了`RuntimeException`及其子类以外,其他的`Exception`类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、`ClassNotFoundException` 、`SQLException`...。 - -**Unchecked Exception** 即 **不受检查异常** ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。 - -`RuntimeException` 及其子类都统称为非受检查异常,例如:`NullPointerException`、`NumberFormatException`(字符串转换为数字)、`ArrayIndexOutOfBoundsException`(数组越界)、`ClassCastException`(类型转换错误)、`ArithmeticException`(算术错误)等。 - -![](./images/unchecked-exception.png) - -### Throwable 类常用方法有哪些? - -- `String getMessage()`: 返回异常发生时的简要描述 -- `String toString()`: 返回异常发生时的详细信息 -- `String getLocalizedMessage()`: 返回异常对象的本地化信息。使用 `Throwable` 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 `getMessage()`返回的结果相同 -- `void printStackTrace()`: 在控制台上打印 `Throwable` 对象封装的异常信息 - -### try-catch-finally 如何使用? - -- **`try`块:** 用于捕获异常。其后可接零个或多个 `catch` 块,如果没有 `catch` 块,则必须跟一个 `finally` 块。 -- **`catch`块:** 用于处理 try 捕获到的异常。 -- **`finally` 块:** 无论是否捕获或处理异常,`finally` 块里的语句都会被执行。当在 `try` 块或 `catch` 块中遇到 `return` 语句时,`finally` 语句块将在方法返回之前被执行。 - -示例: - -```java -try { - System.out.println("Try to do something"); - throw new RuntimeException("RuntimeException"); -} catch (Exception e) { - System.out.println("Catch Exception -> " + e.getMessage()); -} finally { - System.out.println("Finally"); -} -``` - -输出: - -``` -Try to do something -Catch Exception -> RuntimeException -Finally -``` - -**注意:不要在 finally 语句块中使用 return!** 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句不会被执行。 - -示例: - -```java -public static void main(String[] args) { - System.out.println(f(2));; -} - -public static int f(int value) { - try { - return value * value; - } finally { - if (value == 2) { - return 0; - } - } -} -``` - -输出: - -``` -0 -``` - -### finally 中的代码一定会执行吗? - -不一定的!在某些情况下,finally 中的代码不会被执行。 - -就比如说 `finally` 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。 - -```java -try { - System.out.println("Try to do something"); - throw new RuntimeException("RuntimeException"); -} catch (Exception e) { - System.out.println("Catch Exception -> " + e.getMessage()); - // 终止当前正在运行的Java虚拟机 - System.exit(1); -} finally { - System.out.println("Finally"); -} -``` - -输出: - -``` -Try to do something -Catch Exception -> RuntimeException -``` - -另外,在以下 2 种特殊情况下,`finally` 块的代码也不会被执行: - -1. 程序所在的线程死亡。 -2. 关闭 CPU。 - -相关 issue: 。 - -🧗🏻进阶一下:从字节码角度分析`try catch finally`这个语法糖背后的实现原理。 - -### 如何使用 `try-with-resources` 代替`try-catch-finally`? - -1. **适用范围(资源的定义):** 任何实现 `java.lang.AutoCloseable`或者 `java.io.Closeable` 的对象 -2. **关闭资源和 finally 块的执行顺序:** 在 `try-with-resources` 语句中,任何 catch 或 finally 块在声明的资源关闭后运行 - -《Effective Java》中明确指出: - -> 面对必须要关闭的资源,我们总是应该优先使用 `try-with-resources` 而不是`try-finally`。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。`try-with-resources`语句让我们更容易编写必须要关闭的资源的代码,若采用`try-finally`则几乎做不到这点。 - -Java 中类似于`InputStream`、`OutputStream` 、`Scanner` 、`PrintWriter`等的资源都需要我们调用`close()`方法来手动关闭,一般情况下我们都是通过`try-catch-finally`语句来实现这个需求,如下: - -```java -//读取文本文件的内容 -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` 语句改造上面的代码: - -```java -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-catch-finally`可能会带来很多问题。 - -通过使用分号分隔,可以在`try-with-resources`块中声明多个资源。 - -```java -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(); - } -``` - -## I/O 流 - -### 什么是序列化?什么是反序列化? - -如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。 - -简单来说: - -- **序列化**: 将数据结构或对象转换成二进制字节流的过程 -- **反序列化**:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程 - -对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。 - -维基百科是如是介绍序列化的: - -> **序列化**(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。 - -综上:**序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。** - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2020-8/a478c74d-2c48-40ae-9374-87aacf05188c.png) - -

https://www.corejavaguru.com/java/serialization/interview-questions-1

- -### Java 序列化中如果有些字段不想进行序列化,怎么办? - -对于不想进行序列化的变量,使用 `transient` 关键字修饰。 - -`transient` 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 `transient` 修饰的变量值不会被持久化和恢复。 - -关于 `transient` 还有几点注意: - -- `transient` 只能修饰变量,不能修饰类和方法。 -- `transient` 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 `int` 类型,那么反序列后结果就是 `0`。 -- `static` 变量因为不属于任何对象(Object),所以无论有没有 `transient` 关键字修饰,均不会被序列化。 - -### 获取用键盘输入常用的两种方法 - -方法 1:通过 `Scanner` - -```java -Scanner input = new Scanner(System.in); -String s = input.nextLine(); -input.close(); -``` - -方法 2:通过 `BufferedReader` - -```java -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: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 - -按操作方式分类结构图: - -![IO-操作方式分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作方式分类.png) - -按操作对象分类结构图: - -![IO-操作对象分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作对象分类.png) - -### 既然有了字节流,为什么还要有字符流? - -问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?** - -回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 - ## 参考 - https://stackoverflow.com/questions/1906445/what-is-the-difference-between-jdk-and-jre diff --git a/docs/java/basis/java-basic-questions-02.md b/docs/java/basis/java-basic-questions-02.md new file mode 100644 index 00000000..8cc2a6bc --- /dev/null +++ b/docs/java/basis/java-basic-questions-02.md @@ -0,0 +1,286 @@ +--- +title: Java基础知识&面试题总结(中) +category: Java +tag: + - Java基础 +--- + +## 面向对象基础 + +### 面向对象和面向过程的区别 + +两者的主要区别在于解决问题的方式不同: + +- 面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。 +- 面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。 + +另外,面向对象开发的程序一般更易维护、易复用、易扩展。 + +相关 issue : [面向过程 :面向过程性能比面向对象高??](https://github.com/Snailclimb/JavaGuide/issues/431) + +### 成员变量与局部变量的区别有哪些? + +- **语法形式** :从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 `public`,`private`,`static` 等修饰符所修饰,而局部变量不能被访问控制修饰符及 `static` 所修饰;但是,成员变量和局部变量都能被 `final` 所修饰。 +- **存储方式** :从变量在内存中的存储方式来看,如果成员变量是使用 `static` 修饰的,那么这个成员变量是属于类的,如果没有使用 `static` 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 +- **生存时间** :从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。 +- **默认值** :从变量是否有默认值来看,成员变量如果没有被赋初,则会自动以类型的默认值而赋值(一种情况例外:被 `final` 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。 + +### 创建一个对象用什么运算符?对象实体与对象引用有何不同? + +new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。 + +一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。 + +### 对象的相等与指向他们的引用相等,两者有什么不同? + +- 对象的相等一般比较的是内存中存放的内容是否相等。 +- 引用相等一般比较的是他们指向的内存地址是否相等。 + +### 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? + +构造方法主要作用是完成对类对象的初始化工作。 + +如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了,这时候,就不能直接 new 一个对象而不传递参数了,所以我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑。 + +### 构造方法有哪些特点?是否可被 override? + +构造方法特点如下: + +- 名字与类名相同。 +- 没有返回值,但不能用 void 声明构造函数。 +- 生成类的对象时自动执行,无需调用。 + +构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。 + +### 面向对象三大特征 + +#### 封装 + +封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。就好像我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。就好像如果没有空调遥控器,那么我们就无法操控空凋制冷,空调本身就没有意义了(当然现在还有很多其他方法 ,这里只是为了举例子)。 + +```java +public class Student { + private int id;//id属性私有化 + private String name;//name属性私有化 + + //获取id的方法 + public int getId() { + return id; + } + + //设置id的方法 + public void setId(int id) { + this.id = id; + } + + //获取name的方法 + public String getName() { + return name; + } + + //设置name的方法 + public void setName(String name) { + this.name = name; + } +} +``` + +#### 继承 + +不同类型的对象,相互之间经常有一定数量的共同点。例如,小明同学、小红同学、小李同学,都共享学生的特性(班级、学号等)。同时,每一个对象还定义了额外的特性使得他们与众不同。例如小明的数学比较好,小红的性格惹人喜爱;小李的力气比较大。继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。 + +**关于继承如下 3 点请记住:** + +1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,**只是拥有**。 +2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 +3. 子类可以用自己的方式实现父类的方法。(以后介绍)。 + +#### 多态 + +多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。 + +**多态的特点:** + +- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系; +- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定; +- 多态不能调用“只在子类存在但在父类不存在”的方法; +- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。 + +### 深拷贝和浅拷贝区别了解吗?什么是引用拷贝? + +关于深拷贝和浅拷贝区别,我这里先给结论: + +- **浅拷贝**:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。 +- **深拷贝** :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。 + +上面的结论没有完全理解的话也没关系,我们来看一个具体的案例! + +**浅拷贝** + +浅拷贝的示例代码如下,我们这里实现了 `Cloneable` 接口,并重写了 `clone()` 方法。 + +`clone()` 方法的实现很简单,直接调用的是父类 `Object` 的 `clone()` 方法。 + +```java +public class Address implements Cloneable{ + private final String name; + // 省略构造函数、Getter&Setter方法 + @Override + public Address clone() { + try { + return (Address) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } + } +} + +public class Person implements Cloneable { + private Address address; + // 省略构造函数、Getter&Setter方法 + @Override + public Person clone() { + try { + Person person = (Person) super.clone(); + return person; + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } + } +} +``` + +测试 : + +```java +Person person1 = new Person(new Address("武汉")); +Person person1Copy = person1.clone(); +// true +System.out.println(person1.getAddress() == person1Copy.getAddress()); +``` + +从输出结构就可以看出, `person1` 的克隆对象和 `person1` 使用的仍然是同一个 `Address` 对象。 + +**深拷贝** + +这里我们简单对 `Person` 类的 `clone()` 方法进行修改,连带着要把 `Person` 对象内部的 `Address` 对象一起复制。 + +```java +@Override +public Person clone() { + try { + Person person = (Person) super.clone(); + person.setAddress(person.getAddress().clone()); + return person; + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } +} +``` + +测试 : + +```java +Person person1 = new Person(new Address("武汉")); +Person person1Copy = person1.clone(); +// false +System.out.println(person1.getAddress() == person1Copy.getAddress()); +``` + +从输出结构就可以看出,虽然 `person1` 的克隆对象和 `person1` 包含的 `Address` 对象已经是不同的了。 + +**那什么是引用拷贝呢?** 简单来说,引用拷贝就是两个不同的引用指向同一个对象。 + +我专门画了一张图来描述浅拷贝、深拷贝、引用拷贝: + +![](./images/shallow&deep-copy.png) + +## Java 常见对象 + +### String、StringBuffer、StringBuilder的区别?String 为什么是不可变的? + +**可变性** + +简单的来说:`String` 类中使用 `final` 关键字修饰字符数组来保存字符串,~~所以`String` 对象是不可变的。~~ + +```java +public final class String implements java.io.Serializable, Comparable, CharSequence { + private final char value[]; + //... +} +``` + +> 🐛 修正 : 我们知道被 `final` 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,`final` 关键字修饰的数组保存字符串并不是 `String` 不可变的根本原因,因为这个数组保存的字符串是可变的(`final` 修饰引用类型变量的情况)。 +> +> `String` 真正不可变有下面几点原因: +> +> 1. 保存字符串的数组被 `final` 修饰且为私有的,并且`String` 类没有提供/暴露修改这个字符串的方法。 +> 2. `String` 类被 `final` 修饰导致其不能被继承,进而避免了子类破坏 `String` 不可变。 +> +> 相关阅读:[如何理解 String 类型值的不可变? - 知乎提问](https://www.zhihu.com/question/20618891/answer/114125846) +> +> 补充(来自[issue 675](https://github.com/Snailclimb/JavaGuide/issues/675)):在 Java 9 之后,`String` 、`StringBuilder` 与 `StringBuffer` 的实现改用 byte 数组存储字符串。 + +`StringBuilder` 与 `StringBuffer` 都继承自 `AbstractStringBuilder` 类,在 `AbstractStringBuilder` 中也是使用字符数组保存字符串,不过没有使用 `final` 和 `private` 关键字修饰,最关键的是这个 `AbstractStringBuilder` 类还提供了很多修改字符串的方法比如 `append` 方法。 + +```java +abstract class AbstractStringBuilder implements Appendable, CharSequence { + char[] value; + public AbstractStringBuilder append(String str) { + if (str == null) + return appendNull(); + int len = str.length(); + ensureCapacityInternal(count + len); + str.getChars(0, len, value, count); + count += len; + return this; + } + //... +} +``` + +**线程安全性** + +`String` 中的对象是不可变的,也就可以理解为常量,线程安全。`AbstractStringBuilder` 是 `StringBuilder` 与 `StringBuffer` 的公共父类,定义了一些字符串的基本操作,如 `expandCapacity`、`append`、`insert`、`indexOf` 等公共方法。`StringBuffer` 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。`StringBuilder` 并没有对方法进行加同步锁,所以是非线程安全的。 + +**性能** + +每次对 `String` 类型进行改变的时候,都会生成一个新的 `String` 对象,然后将指针指向新的 `String` 对象。`StringBuffer` 每次都会对 `StringBuffer` 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 `StringBuilder` 相比使用 `StringBuffer` 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 + +**对于三者使用的总结:** + +1. 操作少量的数据: 适用 `String` +2. 单线程操作字符串缓冲区下操作大量数据: 适用 `StringBuilder` +3. 多线程操作字符串缓冲区下操作大量数据: 适用 `StringBuffer` + +### String#equals() 和 Object#equals() 有何区别? + +`String` 中的 `equals` 方法是被重写过的,比较的是String 字符串的值是否相等。 `Object` 的 `equals` 方法是比较的对象的内存地址。 + +### Object 类的常见方法有哪些? + +Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法: + +```java +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 { }//实例被垃圾回收器回收的时候触发的操作 +``` + diff --git a/docs/java/basis/java-basic-questions-03.md b/docs/java/basis/java-basic-questions-03.md new file mode 100644 index 00000000..0110c1d0 --- /dev/null +++ b/docs/java/basis/java-basic-questions-03.md @@ -0,0 +1,461 @@ +--- +title: Java基础知识&面试题总结(下) +category: Java +tag: + - Java基础 +--- + +## 泛型 + +#### Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符? + +**Java 泛型(generics)** 是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。 + +Java 的泛型是伪泛型,这是因为 Java 在运行期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。 + +```java +List list = new ArrayList<>(); + +list.add(12); +//这里直接添加会报错 +list.add("a"); +Class clazz = list.getClass(); +Method add = clazz.getDeclaredMethod("add", Object.class); +//但是通过反射添加是可以的 +//这就说明在运行期间所有的泛型信息都会被擦掉 +add.invoke(list, "kl"); +System.out.println(list); +``` + +泛型一般有三种使用方式: 泛型类、泛型接口、泛型方法。 + +**1.泛型类**: + +```java +//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 +//在实例化泛型类时,必须指定T的具体类型 +public class Generic { + private T key; + public Generic(T key) { + this.key = key; + } + public T getKey() { + return key; + } +} +``` + +如何实例化泛型类: + +```java +Generic genericInteger = new Generic(123456); +``` + +**2.泛型接口** : + +```java +public interface Generator { + public T method(); +} +``` + +实现泛型接口,不指定类型: + +```java +class GeneratorImpl implements Generator{ + @Override + public T method() { + return null; + } +} +``` + +实现泛型接口,指定类型: + +```java +class GeneratorImpl implements Generator{ + @Override + public String method() { + return "hello"; + } +} +``` + +**3.泛型方法** : + +```java +public static void printArray(E[] inputArray) { + for (E element : inputArray) { + System.out.printf("%s ", element); + } + System.out.println(); +} +``` + +使用: + +```java +// 创建不同类型数组: Integer, Double 和 Character +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` 可根据具体的返回类型动态指定结果的数据类型 +- 定义 `Excel` 处理类 `ExcelUtil` 用于动态指定 `Excel` 导出的数据类型 +- 用于构建集合工具类。参考 `Collections` 中的 `sort`, `binarySearch` 方法 +- ...... + +## 反射 + +### 何为反射? + +如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。 + +反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。 + +通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。 + +### 反射机制优缺点 + +- **优点** : 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利 +- **缺点** :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。[Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow) + +### 反射的应用场景 + +像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。 + +但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。 + +**这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。** + +比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 `Method` 来调用指定的方法。 + +```java +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`注解就读取到配置文件中的值呢?究竟是怎么起作用的呢? + +这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。 + +## 注解 + +`Annontation` (注解) 是Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量。 + +注解本质是一个继承了`Annotation` 的特殊接口: + +```java +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.SOURCE) +public @interface Override { + +} + +public interface Override extends Annotation{ + +} +``` + +注解只有被解析之后才会生效,常见的解析方法有两种: + +- **编译期直接扫描** :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用`@Override` 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 +- **运行期通过反射处理** :像框架中自带的注解(比如 Spring 框架的 `@Value` 、`@Component`)都是通过反射来进行处理的。 + +JDK 提供了很多内置的注解(比如 `@Override` 、`@Deprecated`),同时,我们还可以自定义注解。 + +## 异常 + +**Java 异常类层次结构图概览** : + +![types-of-exceptions-in-java](./images/types-of-exceptions-in-java.png) + + + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-12/Java%E5%BC%82%E5%B8%B8%E7%B1%BB%E5%B1%82%E6%AC%A1%E7%BB%93%E6%9E%84%E5%9B%BE2.png) + +

图片来自:https://chercher.tech/java-programming/exceptions-java

+ +### Exception 和 Error 有什么区别? + +在 Java 中,所有的异常都有一个共同的祖先 `java.lang` 包中的 `Throwable` 类。`Throwable` 类有两个重要的子类: + +- **`Exception`** :程序本身可以处理的异常,可以通过 `catch` 来进行捕获。`Exception` 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。 +- **`Error`** :`Error` 属于程序无法处理的错误 ,我们没办法通过 `catch` 来进行捕获 。例如Java 虚拟机运行错误(`Virtual MachineError`)、虚拟机内存不够错误(`OutOfMemoryError`)、类定义错误(`NoClassDefFoundError`)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。 + +### Checked Exception 和 Unchecked Exception 有什么区别? + +**Checked Exception** 即受检查异常,Java 代码在编译过程中,如果受检查异常没有被 `catch`/`throw` 处理的话,就没办法通过编译 。 + +比如下面这段 IO 操作的代码: + +![](/Users/guide/Documents/GitHub/JavaGuide/docs/java/basis/images/checked-exception.png) + +除了`RuntimeException`及其子类以外,其他的`Exception`类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、`ClassNotFoundException` 、`SQLException`...。 + +**Unchecked Exception** 即 **不受检查异常** ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。 + +`RuntimeException` 及其子类都统称为非受检查异常,例如:`NullPointerException`、`NumberFormatException`(字符串转换为数字)、`ArrayIndexOutOfBoundsException`(数组越界)、`ClassCastException`(类型转换错误)、`ArithmeticException`(算术错误)等。 + +![](/Users/guide/Documents/GitHub/JavaGuide/docs/java/basis/images/unchecked-exception.png) + +### Throwable 类常用方法有哪些? + +- `String getMessage()`: 返回异常发生时的简要描述 +- `String toString()`: 返回异常发生时的详细信息 +- `String getLocalizedMessage()`: 返回异常对象的本地化信息。使用 `Throwable` 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 `getMessage()`返回的结果相同 +- `void printStackTrace()`: 在控制台上打印 `Throwable` 对象封装的异常信息 + +### try-catch-finally 如何使用? + +- **`try`块:** 用于捕获异常。其后可接零个或多个 `catch` 块,如果没有 `catch` 块,则必须跟一个 `finally` 块。 +- **`catch`块:** 用于处理 try 捕获到的异常。 +- **`finally` 块:** 无论是否捕获或处理异常,`finally` 块里的语句都会被执行。当在 `try` 块或 `catch` 块中遇到 `return` 语句时,`finally` 语句块将在方法返回之前被执行。 + +示例: + +```java +try { + System.out.println("Try to do something"); + throw new RuntimeException("RuntimeException"); +} catch (Exception e) { + System.out.println("Catch Exception -> " + e.getMessage()); +} finally { + System.out.println("Finally"); +} +``` + +输出: + +``` +Try to do something +Catch Exception -> RuntimeException +Finally +``` + +**注意:不要在 finally 语句块中使用 return!** 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句不会被执行。 + +示例: + +```java +public static void main(String[] args) { + System.out.println(f(2));; +} + +public static int f(int value) { + try { + return value * value; + } finally { + if (value == 2) { + return 0; + } + } +} +``` + +输出: + +``` +0 +``` + +### finally 中的代码一定会执行吗? + +不一定的!在某些情况下,finally 中的代码不会被执行。 + +就比如说 `finally` 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。 + +```java +try { + System.out.println("Try to do something"); + throw new RuntimeException("RuntimeException"); +} catch (Exception e) { + System.out.println("Catch Exception -> " + e.getMessage()); + // 终止当前正在运行的Java虚拟机 + System.exit(1); +} finally { + System.out.println("Finally"); +} +``` + +输出: + +``` +Try to do something +Catch Exception -> RuntimeException +``` + +另外,在以下 2 种特殊情况下,`finally` 块的代码也不会被执行: + +1. 程序所在的线程死亡。 +2. 关闭 CPU。 + +相关 issue: 。 + +🧗🏻进阶一下:从字节码角度分析`try catch finally`这个语法糖背后的实现原理。 + +### 如何使用 `try-with-resources` 代替`try-catch-finally`? + +1. **适用范围(资源的定义):** 任何实现 `java.lang.AutoCloseable`或者 `java.io.Closeable` 的对象 +2. **关闭资源和 finally 块的执行顺序:** 在 `try-with-resources` 语句中,任何 catch 或 finally 块在声明的资源关闭后运行 + +《Effective Java》中明确指出: + +> 面对必须要关闭的资源,我们总是应该优先使用 `try-with-resources` 而不是`try-finally`。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。`try-with-resources`语句让我们更容易编写必须要关闭的资源的代码,若采用`try-finally`则几乎做不到这点。 + +Java 中类似于`InputStream`、`OutputStream` 、`Scanner` 、`PrintWriter`等的资源都需要我们调用`close()`方法来手动关闭,一般情况下我们都是通过`try-catch-finally`语句来实现这个需求,如下: + +```java +//读取文本文件的内容 +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` 语句改造上面的代码: + +```java +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-catch-finally`可能会带来很多问题。 + +通过使用分号分隔,可以在`try-with-resources`块中声明多个资源。 + +```java +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(); + } +``` + +## I/O + +### 什么是序列化?什么是反序列化? + +如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。 + +简单来说: + +- **序列化**: 将数据结构或对象转换成二进制字节流的过程 +- **反序列化**:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程 + +对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。 + +维基百科是如是介绍序列化的: + +> **序列化**(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。 + +综上:**序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。** + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2020-8/a478c74d-2c48-40ae-9374-87aacf05188c.png) + +

https://www.corejavaguru.com/java/serialization/interview-questions-1

+ +### Java 序列化中如果有些字段不想进行序列化,怎么办? + +对于不想进行序列化的变量,使用 `transient` 关键字修饰。 + +`transient` 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 `transient` 修饰的变量值不会被持久化和恢复。 + +关于 `transient` 还有几点注意: + +- `transient` 只能修饰变量,不能修饰类和方法。 +- `transient` 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 `int` 类型,那么反序列后结果就是 `0`。 +- `static` 变量因为不属于任何对象(Object),所以无论有没有 `transient` 关键字修饰,均不会被序列化。 + +### 获取用键盘输入常用的两种方法 + +方法 1:通过 `Scanner` + +```java +Scanner input = new Scanner(System.in); +String s = input.nextLine(); +input.close(); +``` + +方法 2:通过 `BufferedReader` + +```java +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: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 + +按操作方式分类结构图: + +![IO-操作方式分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作方式分类.png) + +按操作对象分类结构图: + +![IO-操作对象分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作对象分类.png) + +### 既然有了字节流,为什么还要有字符流? + +问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?** + +回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 \ No newline at end of file