Java基础知识
基础概念与常识
JVM、JDK、JRE
JVM
Java 虚拟机(JVM)试运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Window,Linux,macOS),目的是使用相同的字节码,他们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言 ”一次编辑,随处可以运行“的关键所在
JVM 并不是只有一种!只要满足 JVM 规范,每个公司,组织或个人都可以开发自己的专属 JVM。
JDK 和 JRE
JDK 是 java development Kit 缩写,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如javadoc 和 jdb)。它能够创建和编译程序。
JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java类库,Java 命令和其他的一些基础构建。但是,他不能用于创建程序。
字节码的好处
在 Java 中, JVM 可以理解的代码就叫做字节码(即扩展名为 .class
的文件)它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释性语言可移植性的特点。所以,Java 程序运行时相对来说还是高效的,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无需重新编译便可在多种不同操作系统的计算机上运行。
Java 程序从源代码到运行的过程如下图所示
注意:.class
这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(just-in-time compilation)编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定高于 Java 解释器。这也解释了我们为什么会经常说 Java是编译与解释共存的语言
解释型与编译型
高级语言按照程序的执行方式分为两种:
- 编译型:编译型语言 会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译型语言有 C,C++,Go,Rust
- 解释型:解释型语言会通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释型语言有 Python,JavaScript,PHP等
基本语法
字符型常量和字符串常量的区别
- 形式: 字符常量是单引号引起的一个字符;字符串常量是双引号引起的 0 个或 若干个字符
- 含义: 字符常量相当于一个整型值(ASCII值),可以参加表达式运算;字符串常量代表一个地址值(该字符串在内存中存放位置)
- **占内存大小:**字符常量只占 2 个字节;字符串常量占若干个字节
标识符和关键字
编写程序的时候,需要大量的为程序,类,变量,方法等取名字,于是就有了标识符,标识符就是一个名字。但是有一些标识符,Java语言已经赋予了其特殊的含义,只能用于特定的地方,这种特殊的标识符就是关键字。因此,关键字是被赋予特殊含义的标识符。
静态方法为什么不能调用非静态成员?
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化后才存在,需要通过类的实例对象去访问。
- 在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在非静态成员,属于非法操作。
静态方法和实例方法有何不同?
1. 调用方式
在外部调用静态方法时,可以使用 类名.方法名
的方式,可以使用 对象.方法名
,而实例方法只有对象.方法名
这一种方式。也就是说,调用静态方法可以无需创建对象。
2. 访问类成员是否存在限制
静态方法在访问本类的成员时,只允许静态成员(即静态成员和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制
重写
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
- 返回值类型,方法名,参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
- 如果父类方法访问修饰符为
private/final/static
则子类就不能重写该方法,但是被static
修饰的方法能够被再次声明。 - 给构造方法无法被重写
综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变
区别点 | 重载方法 | 重写方法 |
---|---|---|
发生范围 | 同一个类 | 子类 |
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可修改 | 子类方法返回值类型应比父类返回值类型更小或相等 |
异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等 |
访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
发生阶段 | 编译期 | 运行期 |
方法的重写要遵循“两同两小一大”
- 两同: 即方法名相同,形参列表相同
- 两小: 指的是子类方法返回值类型应比父类返回值类型更小或相等,子类方法声明抛出的异常比父类方法声明抛出的异常类更小或相等
- 一大: 指的是子类方法的访问权限应比父类的访问权限更大或相等
注意: 如果方法的返回类型是 void 和基本数据类型,则返回重写时不可修改,但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。
public class Hero {
public String name() {
return "超级英雄";
}
}
public class SuperMan extends Hero{
@Override
public String name() {
return "超人";
}
public Hero hero() {
return new Hero();
}
}
public class SuperSuperMan extends SuperMan {
public String name() {
return "超级超级英雄";
}
@Override
public SuperMan hero() {
return new SuperMan();
}
}
泛型
Java泛型是JDK5中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许我们在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
Java 的泛型是伪泛型,这是因为 Java 在运行期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除。
泛型一般有三种使用方式:泛型类,泛型接口,泛型方法
1. 泛型类
// 此处T可以随便写任意标识,常见的如 T,E,K,V 等形式的参数常用于表示泛型
// 在实例化泛型时,必须指定 T 的具体参数类型
public class Generic<T> {
private T key;
public Generic(T key){
this.key = key;
}
public T getKey(){
return key;
}
}
如何实例化泛型类:
Generic<Integer> genericInteger = new Generic<Integer>(123456);
2. 泛型接口
public interface Generator<T> {
public T method();
}
实现泛型接口,不指定类型:
class GeneratorImpl<T> implements Generator<T> {
public T method() {
return null;
}
}
实现泛型接口,指定类型:
class GeneratorImpl implements Generator<String> {
public String method() {
return "hello";
}
}
3. 泛型方法
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);
== 和 equals() 的区别
==
对于基本类型和引用类型的效果是不同的
- 对于基本数据类型来说,
==
比较的是值 - 对于引用数据类型来说,
==
比较的是对象的内存地址
因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型给变量存的值是对象的地址
equals()
作用不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()
方法存在于Object
类中,而 Object
类是所有类的直接或间接父类
Object
类 equals()
方法:
public boolean equals(Object obj) {
return (this == obj);
}
equals()
方法存在两种使用情况:
- 类没有覆盖
equals()
方法:通过equals()
比较该类的两个对象时,等价于通过”==“比较这两个对象,使用的默认是Object
类equals()
方法。 - 类覆盖了
equals()
方法: 一般我们都覆盖equals()
方法来比较两个对象中的属性是否相等;若它们的属相相同,则返回 true(即,认为这两个对象相等)
举例:
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b 为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
System.out.println(aa == bb);// true
System.out.println(a == b);// false
System.out.println(a.equals(b));// true
System.out.println(42 == 42.0);// true
hashCode() 与 equals()
hashCode() 介绍
hashCode()
的作用是获取哈希码(int
整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()
定义在 JDK 的 Object
类中,这就以为着 Java 中的任何类都包含有 hashCode()
函数。另外要注意的是:Object
的 hashCode()
方法是本地方法,该方法通常用来将对象的内存地址转换为整数之后返回。
public native int hashCode();
为什么要有 hashCode?
当我们把对象加入
HashSet
时,HashSet
会先计算对象的hashcode
值来判断对象加入的位置,同时也会与其他已经加入的对象的hashcode
值作比较,如果没有相符的hashcode
,HashSet
会假设对象没有重复出现。但是如果发现有相同的hashcode
值的对象,这时会调用equals()
方法来检查hashcode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了equals
的次数,相应就大大提高了执行速度。
为什么重写 equals() 时必须重写 hashCode() 方法?
hashCode()
的默认行为时对堆上的对象产生独特值。如果没有重写 hashCode
,则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
简单来说就是:如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?
因为 hashCode()
所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关
基本数据类型
8 种数据类型的默认值以及所占用的空间大小
基本类型 | 位数 | 字节 | 默认值 |
---|---|---|---|
int | 32 | 4 | 0 |
short | 16 | 2 | 0 |
long | 64 | 8 | 0L |
byte | 8 | 1 | 0 |
char | 16 | 2 | ‘u0000’ |
float | 32 | 4 | 0f |
double | 64 | 8 | 0d |
boolean | 1 | false |
包装类型的常量池技术
Byte
,Short
,Integer
,Long
这4种包装类默认创建了数值 [-128, 127] 的响应类型的缓存数据,Charater
创建了数值在 [0, 127] 范围的缓存数据,Boolean 直接返回 True
or False
。
Integer 缓存源码:
public static Integer valueOf(int i) {
if(i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)]
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static {
int h = 127;
}
}
Boolean 缓存源码
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
两种浮点数类型的包装类 Float
, Double
并没有实现常量池技术
Integer i1 = 33;Integer i2 = 33;System.out.println(i1 == i2); // trueFloat i11 = 333f;Float i22 = 333f;System.out.println(i11 == i22); // falseDouble i3 = 1.2;Double i4 = 1.2;System.out.println(i3 == i4); // false
Integer i1 = 40;Integer i2 = new Integer(40);System.out.println(i1 == i2); // false
Integer i1 = 40
这一行代码会发生装箱,也就是说这行代码等价于 Integer i1 = Integer.valueOf(40)
。因此,i1
直接使用的是常量池中的对象。而 Integer i1 = new Integer(40)
会直接创建新的对象。
因此,答案是 false
所有整形包装类对象之间值的比较,全部使用 equals 方法比较。
什么是自动拆装箱?
- 装箱: 将基本数据类型用它们对应的引用类型包装起来
- 拆箱: 将包装类型转换成基本数据类型
Integer i = 10; // 装箱int n = i; // 拆箱
装箱就是调用了包装类的 valueOf()
方法,拆箱其实就是调用了 xxxValue() 方法。
Integer i = 10
等价于Integer i = Integer.valueOf(10)
int n = i
等价于int n = i.intValue()
Java 面向对象
面向对象和面向过程的区别
- **面向过程:面向过程性能比面向对象高。**因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,如单片机,Linux等一般采用面向过程开发。但是,面向过程没有面向对象易维护,易复用,易扩展
- 面向对象:面向对象易维护,易复用,易扩展。因为面向对象有封装,继承,多态性的特性,所以可以设计出低耦合的系统,使用系统更加灵活,更加易于维护。但是,面向对象性能比面向过程低。
成员变量与局部变量的区别有哪些?
- 从语法上看,成员变量数据类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;参数变量可以被
public
,private
,static
等修饰符修饰,而局部变量不能被访问控制修饰符修饰及static
所修饰;但是,成员变量和局部变量都能被final
修饰 - 从变量在内存中的存储方式来看,如果成员变量是使用
static
修饰的,那么这个成员变量是属于类的,如果没有使用static
修饰,这个成员变量是属于实力的。而对象存在于堆内存,局部变量则存在于栈内存。
创建一个对象用什么运算符?对象实体与对象引用有何不同?
new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
一个对象引用可以指定 0 个或 1 个对象;一个对象也可以有 n 个引用指向它。
对象的相等与指向他们的引用相等,两者有什么不同?
对象的相等,比的是内存中存放的内容是否相等。而引用的相等,比较的是他们指向的内存地址是否相等。
关于继承的重要 3 点
- 子类拥有父类对象的所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有
- 子类可以拥有自己属性和方法,即子类对父类进行扩展。
- 子类可以用户自己的方式实现父类的方法。
多态
多态,表示一个对象具有多种的状态。具体表现为父类引用执行子类的实例
多态的特点:
- 对象类型和引用类型之间具有继承(类)/ 实现(接口)的关系
- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定
- 多态不能调用“只在子类存在但在父类不存在”的方法
- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
深拷贝与浅拷贝的区别
- 浅拷贝: 浅拷贝会在对象堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象公用一个内部对象
- 深拷贝: 深拷贝会完全复制整个对象,包括这个对象所包含的内部对象
反射
反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。
通过反射可以获取任意一个类的所有属性和方法,还可以调用这些方法和属性。
反射机制优缺点
- 优点: 可以让代码更加灵活,为各种框架提供开箱即用的功能提供了便利
- 缺点: 让我们在运行时有了分析操作类的能力,这同时也增加了安全问题。比如可以无视泛型参数的安全检查。另外,反射的性能也要稍微差点,不过,对于框架来说实际是影响不大的。
反射的应用场景
平时大部门的时候是写在业务代码,很少会接触到直接使用反射机制的场景。
但是这并不代表反射没有用。相反,正式因为反射,我们才能这么轻松使用各种框架。像 Spring/Spring Boot、Mybatis 等框架中都大量使用了反射机制。
这些框架中大量使用了动态代理,而动态代理的实现也依赖反射
异常
Java 异常类层次结构图
Throwable 类常用方法
public String getMessage()
返回异常发生时的简要描述public String toString()
返回异常发生时的详细信息public String getLocalizeMesage()
返回异常对象的本地化信息。使用Throwable
子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()
返回的结果相同public void printStackTrace()
在控制台上打印Throwable
对象封装的异常信息
try-catch-finally
try
块: 用于捕获异常。其后可以接零个或多个catch
块,如果没有catch
块,则必须跟一个fainally
块。catch
块: 用于处理 try 捕获到的异常finally
块: 无论是否捕获或处理异常,finally
块里面的语句都会被执行。当在try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行
在以下3中特殊情况下, finally
块不会被执行
- 在
try
或finally
块中使用了System.exit(int)
退出程序,但是,如果System.exit(int)
在异常语句之后,finally
还是会被执行 - 程序所在的线程死亡
- 关闭 CPU
注意: 当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值。
public class Test { public static int f(int value) { try { return value * value; } finally { if (value == 2) { return 0; } } }}
如果调用 f(2)
,返回值将会是 0,因为 fianlly 语句中的返回值覆盖了 try 语句块中的返回值。
I/0流
什么是序列化 / 反序列化?
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者网络传输 Java 对象,这些场景都需要用到序列化。
- 序列化: 将数据结构或对象转换成二进制字节流的过程
- 反序列化: 将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
对于 Java 语言这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class)
序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统,数据库,内存中