Java基础知识点与八股(面向面试-2023秋招提前批记录)
一. JDK、JRE与JVM
- JDK
Java 语言的软件开发工具包,包括了 Java 语言编译器、调试器、类库等工具和组件。JDK 是开发 Java 应用程序的必备工具,它提供了 Java 语言的编译、打包、调试等功能。除此之外,JDK 还包括了一些开发工具,例如 javac、java、jar、javadoc 等,这些工具可以完成 Java 代码的编译、执行、打包和文档生成等任务- JRE
Java 应用程序的运行环境,它包括了 Java 虚拟机(JVM)和 Java 应用程序所需的核心类库等组件。JRE 可以让用户在计算机上运行 Java 应用程序,而不需要安装 JDK。JRE 主要提供了 Java 应用程序的运行环境,包括了 Java 虚拟机(JVM)和 Java 应用程序所需的核心类库等组件- JVM
Java 虚拟机,是 Java 语言的核心组件之一。JVM 是一个虚拟计算机,它可以在计算机上模拟出一个类似于物理计算机的运行环境,可以解释和执行 Java 语言编写的程序。JVM 可以理解并执行 Java 代码,将 Java 代码编译成字节码,并在虚拟机上运行。JVM的目的是使用相同的字节码,它们都会给出相同的结果
二. Java字节码和使用字节码的好处
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机,字节码是一种中间代码,它是 Java 语言编译后的代码。Java 源代码首先会被编译成字节码,然后再由 Java 虚拟机(JVM)解释执行。
使用字节码的好处如下:
- 可移植性强:字节码是一种中间代码,它不依赖于任何特定的硬件平台和操作系统,因此具有很高的可移植性,可以在不同的平台上运行。
- 安全性高:Java 字节码在执行时可以被 JVM 进行安全性检查,可以防止恶意代码的执行,从而提高了 Java 应用程序的安全性。
- 加载速度快:Java 字节码可以通过网络下载,然后在本地机器上执行,因为字节码文件较小,所以可以快速加载,减少了应用程序的启动时间。
- 实现方便:Java 字节码可以通过各种方式生成,例如编写 Java 代码并编译成字节码,或使用其他语言编写代码并将其编译为 Java 字节码,这使得 Java 语言的实现变得非常灵活和方便。
- 跨平台性强:由于 Java 字节码可以在不同的平台上运行,所以它可以用于开发跨平台应用程序,这是 Java 的一个重要特性,也是 Java 成为一种流行编程语言的重要原因之一。
三. JIT
JIT(Just-In-Time)是 Java 虚拟机(JVM)的一种执行模式,它是在运行时动态编译 Java 字节码为本地机器码,并执行本地机器码的过程。JIT 技术是 Java 的一个重要特性,它可以提高 Java 应用程序的性能,使其更加高效
JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以引进了 JIT(just-in-time compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而机器码的运行效率肯定是高于 Java 解释器的。这也解释了为什么经常会说 Java 是编译与解释共存的语言
JIT技术的优点:
- 提高性能:JIT 技术可以将频繁执行的代码编译成本地机器码,从而提高 Java 应用程序的性能。
- 动态优化:JIT 技术可以根据代码的执行情况,动态地优化代码,从而提高 Java 应用程序的性能。
- 节省内存:JIT 技术可以将一部分代码编译成本地机器码,从而减少 Java 应用程序的内存占用。
- 跨平台性:JIT 技术可以将 Java 字节码编译成本地机器码,从而实现跨平台性
四. AOT
AOT(Ahead-Of-Time)技术是一种在编译时将代码编译成本地机器码的技术,与 JIT 技术不同,JIT
技术是在运行时动态编译代码成本地机器码。AOT 技术可以将 Java 应用程序的性能提高到与 C/C++ 应用程序相当的水平。
AOT 技术的工作原理如下:
- Java 应用程序被编译成 Java 字节码。
- AOT 编译器将 Java 字节码编译成本地机器码,并生成一个可执行文件或共享库。
- 在运行时,操作系统将可执行文件或共享库加载到内存中,并直接执行本地机器码。
AOT 技术的优点如下:
- 提高性能:AOT 技术可以将 Java 应用程序的性能提高到与 C/C++ 应用程序相当的水平。
- 减少启动时间:由于 AOT 技术在编译时就将代码编译成本地机器码,因此可以减少应用程序的启动时间。
- 减少内存占用:AOT 技术可以将一部分代码编译成本地机器码,从而减少 Java 应用程序的内存占用。
- 跨平台性:AOT 技术可以将 Java 应用程序编译成本地机器码,从而实现跨平台性。
AOT(Ahead-Of-Time)技术虽然有很多优点,但也存在一些缺点,主要包括以下几点:
- 编译时间长:由于 AOT 技术在编译时将代码编译成本地机器码,因此编译时间较长,可能会导致开发周期增加。
- 可移植性差:AOT 技术将代码编译成本地机器码,因此生成的可执行文件或共享库不具有跨平台性,需要为每个平台生成不同的可执行文件或共享库。
- 难以优化:由于 AOT 技术将代码编译成本地机器码,因此难以在运行时动态优化代码,可能会导致性能不如 JIT 技术。
- 占用磁盘空间多:由于 AOT 技术需要将代码编译成本地机器码,因此生成的可执行文件或共享库较大,占用磁盘空间多。
- 不支持动态代码生成:由于 AOT 技术是在编译时将代码编译成本地机器码,因此不支持动态代码生成,可能会影响一些应用程序的功能
五. 编译型与解释型语言
- 编译型: 编译型语言open in new window 会通过编译器open in new window将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
- 解释型: 解释型语言open in new window会通过解释器open in new window一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等
为什么说Java 语言“编译与解释并存”?
这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class 文件),这种字节码必须由 Java 解释器来解释执行
六. Java运算符
1.& 与 && 的区别
&& 具有短路性,即如果第一个表达式为 false,则直接返回 false,不再判断第二个表达式
&可以作为按位与运算符:用于二进制的计算,只有对应的两个二进位均为1时,结果位才为1 ,否则为0
2.a++与++a
一句口诀:“符号在前就先加/减,符号在后就后加/减”
3.java位运算符
移位操作中,被操作的数据被视为二进制数,移位就是将其向左或向右移动若干位的运算。移位运算符只作用于整形变量,分为两类,第一类是 long 类型,long 类型长度为 8 字节 64 位;第二类为 int 类,int 长度为 4 字节 32 位,移位操作符实际上支持的类型只有int和long,编译器在对short、byte、char类型进行移位前,都会将其转换为int类型再操作
- 左移运算符 <<:向左移若干位,高位丢弃,低位补零。
- 右移运算符 >>:向右移若干位,高位补符号位,低位丢弃。
- 无符号右移 >>>:无符号右移,忽略符号位,空位均用 0 补齐,最终结果必为非负数
其他位运算符
&:对两个二进制数的每一位进行逻辑与运算,结果为1的位需要同时满足两个数的对应位都为1
|:对两个二进制数的每一位进行逻辑或运算,结果为1的位只需要两个数的对应位中有一个为1即可
^:对两个二进制数的每一位进行逻辑异或运算,结果为1的位需要两个数的对应位不相同
~:对一个二进制数的每一位进行取反操作,即0变为1,1变为0
4.a = a + b 与 a += b 的区别
+= 操作符会进行隐式自动类型转换,此处 a += b 隐式地将加操作的结果类型强制转换为持有结果的类型,而 a = a + b 则不会自动进行类型转换
七. Java的八种数据类型
八. 成员变量与局部变量
成员变量可以分为两类:实例变量与类变量
类变量:
类变量(Class Variables),也称为静态变量(Static Variables):类变量是在类中使用 static 关键字声明的变量,它属于整个类,而不是某个对象,也就是说,不需要创建对象就可以访问类变量。类变量的值在所有对象中都是相同的,只会被初始化一次。类变量可以通过类名直接访问,也可以通过对象访问。
实例变量:
实例变量是在类中定义的变量,属于某个对象,每个对象都有自己的一份实例变量副本,它们的值可以相互独立地改变。实例变量必须在对象创建后才能被访问。
局部变量与成员变量的区别:
- 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在(类变量的生产周期随着类的创建而创建,随着类的销毁而销毁),而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡
- 从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值
- 从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存(被static修饰的成员变量存储在方法区),局部变量则存在于栈内存
- 从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰
九. 面向对象和面向过程的区别
- 抽象程度不同:
面向对象是一种更高级别的抽象方法,它将程序中的数据和操作封装在一个对象中,通过对象之间的交互来完成程序的功能。面向过程则更侧重于过程和函数的设计,将程序分解为一系列函数,通过函数的调用来完成程序的功能。 - 程序设计的思想不同:
面向对象是一种基于对象的思想,它更关注数据和行为之间的关系,通过类和对象来表达现实世界的概念和关系,能够更好地描述和解决实际问题。面向过程则更关注程序的执行过程,通过函数的调用和数据传递来完成任务。 - 代码复用和维护的方式不同:
面向对象的设计思想可以更好地支持代码的复用和维护,通过继承、封装和多态等特性,可以减少代码的重复,提高代码的可读性和可维护性。面向过程则更容易产生大量的重复代码和冗余代码,增加程序的复杂度和维护难度。 - 开发效率和程序性能的权衡不同:
面向对象的设计思想可以提高开发效率和程序可靠性,但是也会带来一定的性能损失,因为对象之间的交互需要额外的内存和时间开销。面向过程则更关注程序的执行效率和性能,但是也会牺牲一定的开发效率和代码的可读性。
面向对象和面向过程并不是绝对的对立关系,很多编程语言都支持两种编程思想的混合使用。
在实际开发中,选择面向对象还是面向过程的编程思想,需要根据具体的问题和需求来决定,不同的思想都有其优缺点和适用范围
十. 面向对象三大特征
1. 封装
封装是指将对象的属性和方法封装在一起,通过访问权限控制来保护对象的内部状态,同时隐藏对象的实现细节。封装可以提高代码的可维护性和可读性,减少代码的耦合度
在 Java 中,可以使用访问修饰符(public、private、protected)来控制成员变量和方法的访问权限,从而实现封装
2. 继承
继承是指通过一个已有的类来派生出一个新的类,新的类可以继承原有类的所有属性和方法,并可以添加自己的属性和方法。继承可以提高代码的复用性和可扩展性,避免重复编写代码
在 Java 中,可以使用 extends 关键字来实现继承,子类可以继承父类的所有非私有成员变量和方法
3. 多态
多态是指同一种类型的对象,在不同的情况下,可以表现出不同的行为。多态可以提高代码的灵活性和可扩展性,使程序更加易于维护和扩展
在 Java 中,多态可以通过方法重载、方法覆盖、接口实现和抽象类等方式来实现
多态补充
十一. Java访问控制符
十二. 接口和抽象类
- 共同点: 都不能被实例化。都可以包含抽象方法。都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)
- 区别: 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。一个类只能继承一个类,但是可以实现多个接口。接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值
接口与抽象类补充
十三. Java构造方法
- 构造方法的特点
- 名字与类名相同
- 没有返回值,但不能用 void 声明构造函数
- 生成类的对象时自动执行,无需调用
- 构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况
- 如果一个类没有声明构造方法,该程序能正确执行吗
构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。
如果我们自己添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参数的构造方法了。我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑 - 构造方法可以调用非静态方法吗
在构造方法中可以调用非静态方法,但是需要注意构造方法的执行顺序,因为构造方法是用于初始化对象的,如果在构造方法中调用了非静态方法,需要保证对象已经被完全初始化,否则可能会出现空指针异常等问题
十四. 深拷贝、浅拷贝与引用拷贝
1. 浅拷贝(Shallow Copy):
浅拷贝是指将一个对象复制到另一个对象中,新对象和原对象共享同一块内存地址,对其中一个对象的修改会影响到另一个对象。浅拷贝只复制基本数据类型和对象的引用,而不复制对象本身。
在 Java 中,可以使用 Object 类的 clone() 方法来实现浅拷贝,但是需要实现 Cloneable 接口,否则会抛出 CloneNotSupportedException 异常。
class Person implements Cloneable {
public String name;
public Address address;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Address {
public String city;
}
Person p1 = new Person();
p1.name = "Tom";
p1.address = new Address();
p1.address.city = "Beijing";
Person p2 = (Person) p1.clone();
p2.name = "Jerry";
p2.address.city = "Shanghai"; // 修改 p2 的地址,会影响到 p1 的地址
2. 深拷贝(Deep Copy):
深拷贝是指将一个对象复制到另一个对象中,新对象和原对象不共享同一块内存地址,对其中一个对象的修改不会影响到另一个对象。深拷贝会复制基本数据类型和对象本身。
在 Java 中,可以使用序列化和反序列化、使用第三方库(如 Apache Commons、Google Guava 等)或手动实现来实现深拷贝
class Person implements Serializable {
public String name;
public Address address;
public Person deepClone() throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Person) ois.readObject();
}
}
class Address implements Serializable {
public String city;
}
Person p1 = new Person();
p1.name = "Tom";
p1.address = new Address();
p1.address.city = "Beijing";
Person p2 = p1.deepClone();
p2.name = "Jerry";
p2.address.city = "Shanghai"; // 修改 p2 的地址,不会影响到 p1 的地址
3. 引用拷贝:
引用拷贝就是两个不同的引用指向同一个对象
十五. == 和 equals() 的区别
1. == 对于基本类型和引用类型的作用效果
- 对于基本数据类型来说,== 比较的是值
- 对于引用数据类型来说,== 比较的是对象的内存地址
equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法
equals()的作用是判断两个对象是否相等。当我们重写equals()的时候,可千万不好将它的作用给改变了!
2. equals() 方法存在两种使用情况:
- 类没有重写 equals()方法:通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法
- 类重写了 equals()方法:一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)
3. String类equals()方法
十六. hashCode()
1. hashCode()的作用
- hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置
- hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数
- 虽然,每个Java类都包含hashCode() 函数。但是,仅仅当创建并某个“类的散列表”(关于“散列表”见下面说明)时,该类的hashCode() 才有用(作用是:确定该类的每一个对象在散列表中的位置;其它情况下(例如,创建类的单个对象,或者创建类的对象数组等等),类的hashCode() 没有作用
散列表,指的是:Java集合中本质是散列表的类,如HashMap,Hashtable,HashSet
2. 为什么要有 hashCode()方法
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值作比较,如果没有相符的 hashCode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度
3. 两个对象的hashCode值相等,为什么它们不一定相等
因为 hashCode() 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode )。
- 如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)
- 如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等
- 如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等
4. 为什么重写 equals 方法必须重写 hashcode 方法
equals 方法是用来比较对象大小是否相等的方法,hashcode 方法是用来判断每个对象 hash 值的一种方法。如果只重写 equals 方法而不重写 hashcode 方法,很可能会造成两个不同的对象,它们的 hashcode 也相等,造成冲突
十七. String
十八. 异常
在Java中,异常(Exception)是指在程序运行过程中出现了意外情况或错误,导致程序无法正常执行的情况
所有的异常都有一个共同的祖先 Throwable(可抛出)Throwable 指定代码中可用异常传播机制通过 Java 应用程序传输的任何问题的共性
- Error(错误): 是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述
- Exception(异常): 是程序本身可以处理的异常。Exception 类有一个重要的子类RuntimeException。RuntimeException 类及其子类表示“JVM 常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException
- 异常的分类
- 可查异常(编译器要求必须处置的异常):正确的程序在运行中,很容易出现的、情理可容的异常状况。除了Exception中的RuntimeException及RuntimeException的子类以外,其他的Exception类及其子类(例如:IOException和ClassNotFoundException)都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。
- 不可查异常(编译器不要求强制处置的异常):包括运行时异常(RuntimeException与其子类)和错误(Error)。RuntimeException表示编译器不会检查程序是否对RuntimeException作了处理,在程序中不必捕获RuntimException类型的异常,也不必在方法体声明抛出RuntimeException类。RuntimeException发生的时候,表示程序中出现了编程错误,所以应该找出错误修改程序,而不是去捕获RuntimeException
- 相关面试题(待补充)
十九. 泛型
泛型的本质是为了将类型参数化, 也就是说在泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法
1. 泛型的使用场景
集合类、自定义接口通用返回结果、处理Excel动态获取数据
2. 泛型标识
T :代表一般的任何类。
E :代表 Element 元素的意思,或Exception 异常的意思。
K :代表 Key的意思。
V :代表 Value 的意思,通常与 K 一起配合使用。
S :代表 Subtype 的意思
3. 泛型类
泛型类中,类型参数定义的位置
- 非静态的成员属性类型
- 非静态方法的形参类型(包括非静态成员方法和构造器)
- 非静态的成员方法的返回值类型
泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数
泛型类中的类型参数的确定是在创建泛型类对象的时候
而静态变量和静态方法在类加载时已经初始化,直接使用类名调用;在泛型类的类型参数未确定时,静态成员有可能被调用,因此泛型类的类型参数是不能在静态成员中使用的
静态泛型方法中可以使用自身的方法签名中新定义的类型参数,而不能使用泛型类中定义的类型参数
4. 泛型接口
泛型接口中的类型参数,在该接口被继承或者被实现时确定
一个类或接口实现或继承一个泛型接口,若是没有确定泛型接口中的类型参数,也可以将类或接口也定义为泛型类或泛型接口,其声明的类型参数必须要和被实现或继承的接口中的类型参数相同
5. 泛型方法
基本语法如下:
public <类型参数> 返回类型 方法名(类型参数 变量名) {
...
}
- 当在一个方法签名中的返回值前面声明了一个 < T > 时,该方法就被声明为一个泛型方法。< T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。当然,泛型方法中也可以使用泛型类中定义的泛型参数
- 泛型类中定义的类型参数和泛型方法中定义的类型参数是相互独立的,它们一点关系都没有
- 如果泛型类中定义的类型参数标识和泛型方法中定义的类型参数标识都为< T >,但它们彼此之间是相互独立的。也就是说,泛型方法始终以自己声明的类型参数为准
- 静态成员中不能使用泛型类定义的类型参数,但我们可以将静态成员方法定义为一个泛型方法
6. 泛型的使用
〇 泛型类: 在创建类的对象的时候确定类型参数的具体类型
〇 泛型方法: 在调用方法的时候再确定类型参数的具体类型
泛型方法签名中声明的类型参数只能在该方法里使用,而泛型接口、泛型类中声明的类型参数则可以在整个接口、类中使用。
当调用泛型方法时,根据外部传入的实际对象的数据类型,编译器就可以判断出类型参数 T所代表的具体数据类型
在调用泛型方法的时候,可以显式地指定类型参数,也可以不指定。
当泛型方法的形参列表中有多个类型参数时,在不指定类型参数的情况下,方法中声明的的类型参数为泛型方法中的几种类型参数的共同父类的最小级,直到 Object。
在指定了类型参数的时候,传入泛型方法中的实参的数据类型必须为指定数据类型或者其子类
7. 泛型擦除
泛型擦除: 泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段
不是所有的类型被擦除后都以Object进行替换: 大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换;而有一种情况则不是,那就是使用到了 extends 和 super 语法的有界类型参数
泛型补充
∆ Java中的泛型类型擦除?它是如何实现?会带来什么风险?
Java中的泛型是在编译器层面上实现的,而不是在运行时实现的。这意味着Java中的泛型类型信息在编译时会被擦除,而不会被保留到运行时。这个过程称为泛型类型擦除。
具体来说,Java编译器在编译泛型类型的代码时,会将泛型类型擦除为它们的原始类型,例如泛型类将被转换为普通的类,泛型方法将被转换为普通的方法,泛型类型参数将被转换为它们的上限(或者Object类型,如果没有指定上限)。这样,在运行时,Java虚拟机无法识别泛型类型的信息,只能将它们当做普通的类、方法或类型参数来处理
泛型类型擦除的实现方式是通过在编译时将泛型类型替换为它们的原始类型来实现的。例如,泛型类中声明的类型参数会被替换为它们的上限或Object类型,例如List将会被替换为List,而List将会被替换为List
Java中的泛型擦除指的是在编译期间将泛型类型擦除为它们的原始类型,这意味着在运行时无法获取泛型类型的具体信息。泛型擦除可能会给程序带来以下问题:
- 类型安全问题:泛型擦除可能会导致类型安全问题,例如在使用泛型类型的时候,如果传入了错误的类型参数,编译器将无法检测到错误,这可能会导致程序在运行时出现异常。
- 无法获取泛型类型信息:由于泛型擦除,运行时无法获取泛型类型的具体信息,这可能会导致一些问题,例如无法使用反射获取泛型类型的实际类型参数。
- 代码冗余:泛型擦除可能会导致代码冗余,例如在使用泛型类型的时候,需要进行类型转换或者使用Object类型进行处理,这可能会导致代码的可读性和可维护性下降。
- 性能问题:由于泛型擦除,Java编译器在编译泛型类型的代码时需要进行类型擦除,这可能会导致代码的性能下降
二十. 反射
1. 什么是反射
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制
2. 获取Class对象的三种方式
(1)使用对象的getClass()方法:使用该方法可以获取一个对象的Class对象,例如
String str = "Hello";
Class<? extends String> clazz = str.getClass();
(2)使用类的class属性:使用该属性可以获取一个类的Class对象,例如
Class<String> clazz = String.class;
(3)使用Class.forName()方法:使用该方法可以根据类的完整路径名获取Class对象,例如
Class<?> clazz = Class.forName("java.lang.String");
3. 反射机制的作用
- 运行时动态获取类的信息: 在编写代码时,对于类的信息是必须在编译时确定的,但在运行时,有时需要根据某些条件,动态获取某个类的信息,这时就可以使用Java中的反射机制。
- 动态生成对象: 反射机制可以在运行时生成对象,这样就可以根据参数的不同,动态的创建不同的类的实例对象。
- 动态调用方法: 通过反射机制可以调用类中的方法,不论这些方法是否是公共的,也不论这些方法的参数个数和类型是什么,反射机制都具有这样的能力
- 动态修改属性: 利用反射机制可以获取到类中的所有成员变量,并可以对其进行修改
- 实现动态代理: 利用反射机制可以实现代理模式,通过代理对象完成原对象对某些方法的调用,同时也可以在这些方法的调用前后做一些额外的处理
- 实现泛型: 通过Java反射,可以在运行时获取泛型类型的信息,从而实现泛型的功能
4. Class类的方法
‣ 参考链接
5. 反射的优点和缺点 如何避免反射的缺点
Java反射是一种在运行时动态地获取类信息、创建对象、调用方法等的机制。它的优点和缺点如下:
优点:
(1)动态性:Java反射可以在运行时动态地获取类信息、创建对象、调用方法等,使得程序具有更加灵活和动态的特性。
(2)通用性:Java反射适用于任何Java类,可以获取任何类的信息,而不需要预先知道该类的名称和方法。
(3)框架性:Java反射是许多框架和库的基础,例如JavaBean、Spring、Hibernate等。
缺点:
(1)性能问题:Java反射会影响程序的性能,因为它需要在运行时动态地获取类信息、创建对象、调用方法等,而这些操作都比直接调用方法更加耗时。
(2) 安全问题:Java反射可以访问私有方法和字段,可能会导致安全问题。
(3)可维护性问题:Java反射使得程序更加动态和灵活,但也使得程序更加难以维护,因为它可以绕过编译时的类型检查,使得代码更加容易出错。
为了避免Java反射的缺点,可以采取以下措施:
(1) 缓存反射信息:可以将反射信息缓存起来,避免重复获取反射信息,从而提高程序的性能。
(2) 限制反射访问权限:可以使用安全管理器来限制反射访问的权限,避免安全问题。
(3)使用泛型:可以使用泛型来避免绕过编译时类型检查的问题,提高程序的可维护性。
(4) 使用注解:可以使用注解来标记需要反射的方法和字段,从而提高程序的可读性和可维护性。
总之,Java反射是一种强大而灵活的机制,可以使程序更加动态和灵活,但也存在一些缺点,需要注意使用。为了避免反射的缺点,可以采取一些措施来提高程序的性能、安全性和可维护性。
6. 反射获取泛型信息
在Java中,泛型类型在编译时会被擦除,因此在运行时获取泛型类型需要使用反射机制。可以通过以下步骤来获取泛型类型:
(1)获取字段或方法的Type对象:可以通过Field类或Method类的getGenericType()方法来获取字段或方法的Type对象。
(2)判断Type对象是否为ParameterizedType类型:可以通过Type类的instanceof运算符来判断Type对象是否为ParameterizedType类型。
(3)获取ParameterizedType对象:如果Type对象是ParameterizedType类型,则可以将其转换为ParameterizedType类型,并通过其getActualTypeArguments()方法获取泛型参数的Type对象数组。
(4)获取泛型参数的Class对象:通过ParameterizedType对象的getActualTypeArguments()方法获取到的泛型参数的Type对象数组,可以通过递归调用获取到其中的Class对象,从而获取泛型参数的Class对象。
public class MyClass<T> {
private T value;
public T getValue() { return value; }
public void setValue(T value) { this.value = value; }
}
// 获取字段的泛型信息
Field field = MyClass.class.getDeclaredField("value");
Type type = field.getGenericType();
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Type[] typeArguments = parameterizedType.getActualTypeArguments();
for (Type typeArgument : typeArguments) {
Class<?> typeClass = (Class<?>) typeArgument;
System.out.println("泛型参数类型为:" + typeClass.getName());
}
}
二十一. 注解
二十二. Java关键字
1. this与super
(1)this关键字
- this表示的是当前对象或当前正在创建的对象
- 在类中,使用this.属性或this.方法的方式,调用当前对象属性或方法。但是,通常情况下,我们都选择省略this.。如果方法的形参和类的属性同名时,我们必须显式的使用this.变量的方式,表明此变量是属性,而非形参(在构造方法中比较常见)
- 在类的构造器中,可以显式的使用this(形参列表)的方式,调用本类中指定的其它构造器
- 构造器内部,最多只能声明一个this(形参列表),用来调用其它的构造器,并且只可以在首行
- 使用this访问属性和方法时,如果在本类中未找到,会从父类中查找
(2)super关键字
- super来调用父类中的指定操作,其作用是可以在子类中显示地调用父类的结构
- super可用于访问父类中定义的属性、成员方法与构造器
- 当子父类出现同名成员时,可以用super表明调用的是父类中的成员,super的追溯不仅限于直接父类
- super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识
⚪子类的方法或构造器中。通过使用"super.属性"或"super.方法"的方式,显式的调用父类中声明的属性或方法。但是,通常情况下,我们习惯省略"super."
⚪当子类和父类中定义了同名的属性时,如果要想在子类中调用父类中声明的属性,则必须显式的使用"super.属性"的方式,表明调用的是父类中声明的属性
⚪当子类重写了父类中的方法以后,如果想在子类的方法中调用父类中被重写的方法时,则必须显式的使用"super.方法"的方式,表明调用的是父类中被重写的方法
⚪子类中所有的构造器默认都会访问父类中空参数的构造器
⚪当父类中没有空参数的构造器时,子类的构造器必须通过this(参数列表)或者super(参数列表)语句指定调用本类或者父类中相应的构造器,否则编译出错。同时,只能”二选一”,不能同时出现,且必须放在构造器的首行
⚪在类的多个构造器中,至少有一个类的构造器中使用了"super(形参列表)",调用父类中的构造器
2. static
static 表示的概念是 静态的,在 Java 中,static 主要用来:
- 修饰变量,static 修饰的变量称为静态变量、也称为类变量,类变量属于类所有,对于不同的类来说,static 变量只有一份,static 修饰的变量位于方法区中;static 修饰的变量能够直接通过 类名.变量名 来进行访问,不用通过实例化类再进行使用
- 修饰方法,static 修饰的方法被称为静态方法,静态方法能够直接通过 类名.方法名 来使用,在静态方法内部不能使用非静态属性和方法
- static 可以修饰代码块,主要分为两种,一种直接定义在类中,使用 static{},这种被称为静态代码块,一种是在类中定义静态内部类,使用 static class xxx 来进行定义
- static 可以用于静态导包,通过使用 import static xxx 来实现,这种方式一般不推荐使用
- static 可以和单例模式一起使用,通过双重检查锁来实现线程安全的单例模式
3. final
final 是 Java 中的关键字,它表示的意思是 不可变的,在 Java 中,final 主要用来:
- 修饰类,final 修饰的类不能被继承,不能被继承的意思就是不能使用 extends 来继承被 final 修饰的类
- 修饰变量,final 修饰的变量不能被改写,不能被改写的意思有两种,对于基本数据类型来说,final 修饰的变量,其值不能被改变,final 修饰的对象,对象的引用不能被改变,但是对象内部的属性可以被修改
- final 修饰的变量在某种程度上起到了不可变的效果,所以,可以用来保护只读数据,尤其是在并发编程中,因为明确的不能再为 final 变量进行赋值,有利于减少额外的同步开销
- 修饰方法,final 修饰的方法不能被重写
4. instanceof
在Java中,instanceof用于判断一个对象是否是某个类的实例。它的语法格式如下
object instanceof class
其中,object是要判断的对象,class是要判断的类。如果object是class的一个实例或者是class的子类的实例,instanceof运算符返回true,否则返回false
instanceof关键字主要用于判断对象的类型,它可以用于以下一些情况:
- 判断对象是否是某个类的实例
- 判断对象是否实现了某个接口
- 判断对象是否是某个类的子类的实例
- 判断对象是否是某个抽象类的子类的实例
- 判断对象是否是某个接口的子接口的实例
instanceof关键字可以用于类型转换,例如:
if (obj instanceof MyClass) {
MyClass myObj = (MyClass) obj;
// 对myObj进行操作
}
在这个例子中,首先使用instanceof关键字判断obj是否是MyClass的实例,如果是,则将obj强制转换为MyClass类型,并将它赋值给myObj变量,然后可以对myObj进行操作
二十三. Java方法
1. 静态方法为什么不能调用非静态成员
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只- 有在对象实例化之后才存在,需要通过类的实例对象去访问
- 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作
2. 静态方法和实例方法有何不同 - 调用方式
在外部调用静态方法时,可以使用类名.方法名的方式,也可以使用对象.方法名的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。不过,需要注意的是一般不建议使用对象.方法名的方式来调用静态方法。这种方式非常容易造成混淆,静态方法不属于类的某个对象而是属于这个类。因此,一般建议使用类名.方法名的方式来调用静态方法 - 访问类成员是否存在限制
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制
3. 重载和重写有什么区别
重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
- 重载
发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同
编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。 如果编译器找不到匹配的参数, 就会产生编译时错误, 因为根本不存在匹配, 或者没有一个比其他的更好(这个过程被称为重载解析(overloading resolution))。Java 允许重载任何方法, 而不只是构造器方法
- 重写
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写
方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类
如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。构造方法无法被重写
重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变
4. 可变长参数
Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面的这个方法就可以接受 0 个或者多个参数
public static void method1(String... args) {
//......
}
- 遇到方法重载的情况,会优先匹配固定参数的方法,因为固定参数的方法匹配度更高 Java
- 的可变参数编译后实际会被转换成一个数组,我们看编译后生成的 class文件就可以看出来
二十三. Java装箱与拆箱
- 为什么需要包装类
因为基本数据类型不是对象,不能使用类的方法;因此, java针对基本类型提供了它们对应的包装类 - 装箱与拆箱
装箱:由基本数据类型转换到包装类型,调用了valueOf() 方法
拆箱:由包装类型转换到基本数据类型,调用了xxxValue() 方法
自动装箱、拆箱:JDK5.0开始,java提供了自动拆装箱的机制(不需要手动调用构造器或者方法了)
自动拆箱 : 实际上 底层仍然调用了valueOf() 方法
自动装箱 : 实际上 底层仍然调用了xxxValue() 方法
- 包装类型的缓存机制
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False - 基本数据类型与包装类的区别
- 占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小
- 默认值:成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null
- 用途:除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以
- 存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中
- 比较方式:对于基本数据类型来说,== 比较的是值。对于包装数据类型来说,== 比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 equals() 方法
- Java中的包装类的作用
- 提供了基本数据类型的对象表示方式:基本数据类型是Java中的基础数据类型,但是它们不能直接作为对象使用。通过包装类,我们可以将基本数据类型转换为对象,从而可以对它们进行更多的操作。
- 提供了基本数据类型和字符串之间的相互转换:包装类提供了许多方法,可以将基本数据类型和字符串之间进行相互转换。
- 提供了常量和方法:包装类提供了一些常量和方法,例如MAX_VALUE、MIN_VALUE、parseXxx等
- 包装类可以用于泛型,而基本数据类型不支持