说明
本文仅供本人学习,如果有不对的地方欢迎大家一起讨论
记录JAVA的学习路程——JAVA基础
JAVA基础
JAVA特性和优势
简单性、面向对象、可移植性、高性能、分布式、动态性(反射机制)、多线程、安全性、健壮性
JAVA三大版本
javaSE:标准版(桌面程序、控制台开发…)
javaME:嵌入式开发
javaEE:E企业级开发(web端、服务器开发…)
JDK、JRE、JVM
JDK:开发者工具包是为 Java 开发者提供的完整开发环境,包含JRE、JVM
JRE:运行时环境,是一个软件包,它提供了运行 Java 应用程序所需的所有组件。包含JVM
JVM:java虚拟机,是一种抽象计算机,它为 Java 程序提供了运行环境
编译型和解释型
编译型:开发完成以后需要将所有的源代码都转换成可执行程序,也就是“一次编译,无限次运行”。
解释型:每次执行程序都需要一边转换一边执行,用到哪些源代码就将哪些源代码转换成机器码,用不到的不进行任何处理。
类型 | 原理 | 优点 | 缺点 |
---|---|---|---|
编译型语言 | 通过专门的编译器,将所有源代码一次性转换成特定平台(Windows、Linux 等)执行的机器码(以可执行文件的形式存在)。 | 编译一次后,脱离了编译器也可以运行,并且运行效率高。 | 可移植性差,不够灵活。 |
解释型语言 | 由专门的解释器,根据需要将部分源代码临时转换成特定平台的机器码。 | 跨平台性好,通过不同的解释器,将相同的源代码解释成不同平台下的机器码。 | 一边执行一边转换,效率很低。 |
Java既是编译型也是解释性语言,默认采用的是解释器和编译器混合的模式。
Java 和 C++ 的区别?
他们都是面向对象的编程语言,但是也有很多不同的地方:
- Java 不提供指针来直接访问内存,程序内存更加安全
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
- Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
- C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。
注释有哪几种形式?
Java 中的注释有三种:
- 单行注释:通常用于解释方法内某单行代码的作用。“//”
- 多行注释:通常用于解释一段代码的作用。“/* */”
- 文档注释:通常用于生成 Java 开发文档。“/** */”
标识符&关键字
标识符简单来说就是一个名字,包括程序、类、变量、方法等,都有自己的名字
关键字就是java中被赋予特殊意义的保留标识符,它只能被用在特殊的地方
数据类型
java是强类型语言:要求变量的使用要严格符合规范,所有变量都必须先定义才能使用
java的数据类型分为两大类:基本类型、引用类型
基本数据类型
整数类型:byte(1字节)、short(2字节)、int(4字节)、long(8字节)
浮点型:float(4字节)、double(8字节)
字符型:char(2字符)
boolean型(占1位)
引用数据类型
类、接口、数组
Integer和int
Integer对应是int类型的包装类,就是把int类型包装成Object对象,对象封装有很多好处,可以把属性也就是数据跟处理这些数据的方法结合在一起,比如Integer就有parseInt()等方法来专门处理int型相关的数据。
另一个非常重要的原因就是在Java中绝大部分方法或类都是用来处理类类型对象的,如ArrayList集合类就只能以类作为他的存储对象,而这时如果想把一个int型的数据存入list是不可能的,必须把它包装成类,也就是Integer才能被List所接受。所以Integer的存在是很必要的。
Integer和 int 的区别
- 基本类型和引用类型:int是一种基本数据类型,而Integer是一种引用类型。
- 自动装箱和拆箱:Integer作为int的包装类,它可以实现自动装箱和拆箱。
- 空指针异常:另外,int变量可以直接赋值为0,而Integer变量必须通过实例化对象来赋值。如果对一个未经初始化的Integer变量进行操作,就会出现空指针异常。这是因为它被赋予了null值,而null值是无法进行自动拆箱的。
包装类的缓存机制
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
or False
。
什么是自动拆装箱?
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
成员变量与局部变量
- 语法形式:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被
public
,private
,static
等修饰符所修饰,而局部变量不能被访问控制修饰符及static
所修饰;但是,成员变量和局部变量都能被final
所修饰。 - 存储方式:从变量在内存中的存储方式来看,如果成员变量是使用
static
修饰的,那么这个成员变量是属于类的,如果没有使用static
修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 - 生存时间:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
- 默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被
final
修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
为什么成员变量有默认值?
- 先不考虑变量类型,如果没有默认值会怎样?变量存储的是内存地址对应的任意随机值,程序读取该值运行会出现意外。
- 默认值有两种设置方式:手动和自动,根据第一点,没有手动赋值一定要自动赋值。成员变量在运行时可借助反射等方法手动赋值,而局部变量不行。
- 对于编译器(javac)来说,局部变量没赋值很好判断,可以直接报错。而成员变量可能是运行时赋值,无法判断,误报“没默认值”又会影响用户体验,所以采用自动赋默认值。
静态变量
静态变量就是被static
关键字修饰的变量,它可以被所有的实例共享,无论一个类创建了多少个对象,它们都共享同一个静态变量,静态变量可以通过类名来访问。
通常情况下,静态变量会被final
关键字修饰成为常量。
面向对象
java三大特征
封装、继承和多态
封装
封装是将对象的状态(属性)和行为(方法)组合在一起,并对外隐藏对象的内部细节,只暴露必要的接口。通过封装,可以保护对象的状态不被外部直接修改,增强了代码的安全性和可维护性。
使用private关键字将属性声明为私有的。
提供public的 getter 和 setter 方法来访问和修改私有属性。
继承
继承是面向对象编程中的一个机制,通过继承,一个类可以继承另一个类的属性和方法,从而实现代码的重用。被继承的类称为父类(超类),继承的类称为子类(派生类)。主要使用extends关键字来声明一个类继承另一个类。
多态
多态是指同一个方法在不同对象中具有不同的实现方式。多态性允许对象在不同的上下文中以不同的形式表现。多态可以通过方法重载(Overloading)和方法重写(Overriding)来实现。
方法重载:在同一个类中,方法名相同但参数列表不同。
方法重写:在子类中重新定义父类中的方法。
重载和重写的区别
重载:发生在同一个类中。方法名称相同,参数列表不同。编译时决定调用哪个方法(静态绑定)。
重写:发生在子类和父类之间。方法名称、参数列表和返回类型必须相同(或协变返回类型)。运行时决定调用哪个方法(动态绑定)。
构造器不能被重写:因为构造器不属于类的继承成员,并且它们的名称必须与类名相同。
构造器可以被重载:在同一个类中,可以定义多个构造器,只要它们的参数列表不同。
向上转型和向下转型
- 在Java中,可以使用父类类型的引用指向子类对象,这是向上转型。通过这种方式,可以在运行时期采用不同的子类实现。
- 向下转型是将父类引用转回其子类类型,但在执行前需要确认引用实际指向的对象类型以避免
ClassCastException
。
接口类和抽象类的区别
定义方式
接口:使用interface关键字定义。不能包含实例变量,只能包含常量(public static final)。不能有构造器。
抽象类:使用abstract关键字定义。可以包含实例变量和常量。可以包含抽象方法和具体方法(有方法体的)。可以有构造器。
继承和实现
接口:一个类可以实现多个接口(多重继承)。接口可以继承多个其他接口。
抽象类:一个类只能继承一个抽象类(单继承)。抽象类可以继承其他类和实现接口。
使用场景
接口:用于定义一组不相关类的公共行为。适合用于API设计,提供灵活的多重继承能力。更适合定义能力(能力接口),例如Comparable、Serializable。
抽象类:用于定义一组相关类的公共行为。适合用于提供基础实现和共享代码。更适合定义类之间的层次结构,提供公共的实现和状态。
静态变量和实例变量的区别?
静态变量 | 静态变量 | |
---|---|---|
定义位置 | 用static关键字定义,属于类本身,而不是任何特定的实例public class MyClass { public static int staticVar;} | 不使用static关键字定义,属于类的每个实例public class MyClass { public int instanceVar;} |
内存分配 | 在类加载时分配内存,存储在方法区中,所有实例共享同一个静态变量 | 在每次创建对象时分配内存,存储在堆内存中,每个实例都有自己的实例变量副本。 |
生命周期 | 生命周期与类的生命周期一致,从类加载到类卸载。 | 与对象的生命周期一致,从对象创建到对象被垃圾回收。 |
访问方式 | 通过类名直接访问,也可以通过对象实例访问MyClass.staticVar = 10; // 推荐MyClass obj = new MyClass();obj.staticVar = 10; // 不推荐 | 必须通过对象实例访问。 |
初始化 | 可以在声明时初始化,也可以在静态代码块中初始化。public class MyClass { public static int staticVar = 10; static { staticVar = 20; }} | 在声明时初始化,也可以在构造函数中初始化。public class MyClass { public int instanceVar = 10; public MyClass() { instanceVar = 20; }} |
共享 | 所有实例共享同一个静态变量,因此对静态变量的修改会影响所有实例。 | 每个实例都有独立的实例变量,对一个实例变量的修改不会影响其他实例 |
创建对象的方式
- new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)
- 利用反射机制,动态创建对象(使用newInstance方法)。
- 使用clone()方法,如果类实现了Cloneable接口,可以使用clone()方法复制对象。
- 使用反序列化,通过将对象序列化到文件或流中,然后再进行反序列化来创建对象。
new出的对象什么时候回收?
- 被动回收:通过jvm垃圾回收算法(引用计数法和可达性分析算法)判断对象是否需要回收
- 主动回收:如果对象重写了
finalize()
方法,那么垃圾回收器会在回收该对象之前调用finalize()
方法,对象可以在finalize()
方法中进行一些清理操作。
深拷贝、浅拷贝和引用拷贝
- 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
- 深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
- 引用拷贝:引用拷贝就是两个不同的引用指向同一个对象。
Object
Object类是所有类的父类,主要方法:
/**
* native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
*/
public final native Class<?> getClass()
/**
* native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
*/
public native int hashCode()
/**
* 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
*/
public boolean equals(Object obj)
/**
* native 方法,用于创建并返回当前对象的一份拷贝。
*/
protected native Object clone() throws CloneNotSupportedException
/**
* 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
*/
public String toString()
/**
* native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
*/
public final native void notify()
/**
* native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
*/
public final native void notifyAll()
/**
* native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
*/
public final native void wait(long timeout) throws InterruptedException
/**
* 多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。。
*/
public final void wait(long timeout, int nanos) throws InterruptedException
/**
* 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
*/
public final void wait() throws InterruptedException
/**
* 实例被垃圾回收器回收的时候触发的操作
*/
protected void finalize() throws Throwable { }
== 和 equals() 的区别
==
对于基本类型和引用类型的作用效果是不同的:
- 对于基本数据类型来说,
==
比较的是值。 - 对于引用数据类型来说,
==
比较的是对象的内存地址。
equals()
不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。
equals()
方法存在两种使用情况:
- 类没有重写
equals()
方法:通过equals()
比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是Object
类equals()
方法。 - 类重写了
equals()
方法:一般我们都重写equals()
方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。 - 注意:
String
中的equals
方法是被重写过的,因为Object
的equals
方法是比较的对象的内存地址,而String
的equals
方法比较的是对象的值。
hashCode() 有什么用?
hashCode()
的作用是获取哈希码(int
整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()
和 equals()
都是用于比较两个对象是否相等。
为什么 JDK 还要同时提供这两个方法?
这是因为在一些容器(比如 HashMap
、HashSet
)中,有了 hashCode()
之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HashSet
的过程)!
我们在前面也提到了添加元素进HashSet
的过程,如果 HashSet
在对比的时候,同样的 hashCode
有多个对象,它会继续使用 equals()
来判断是否真的相同。也就是说 hashCode
帮助我们大大缩小了查找成本。
但是,两个对象的hashCode
值相等并不代表两个对象就相等。因为hash值是通过hash函数计算,不可避免的hash值相同的情况,此时就可以使用equals()
来进行值的比较。
String
String、StringBuffer 和 StringBuilder 的区别是什么?
特性 | String | StringBuffer | StringBuilder |
---|---|---|---|
可变性 | 不可变 | 可变 | 可变 |
线程安全性 | 线程安全 | 线程安全 | 非线程安全 |
性能 | 低(频繁修改时) | 中(线程安全开销) | 高(无线程安全开销) |
适用场景 | 字符串内容不变 | 多线程环境中频繁修改 | 单线程环境中频繁修改 |
选择哪个类取决于具体的使用场景。如果字符串内容不变,使用String;如果需要在多线程环境中修改字符串,使用StringBuffer;如果在单线程环境中修改字符串,使用StringBuilder。
String为什么是不可变的
- 保存字符串的数组被
final
修饰且为私有的,并且String
类没有提供/暴露修改这个字符串的方法。 String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
不可变。
字符串拼接用“+”还是StringBuilder
字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象 。因此,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder
以复用,会导致创建过多的 StringBuilder
对象。
字符串常量池
字符串常量池:是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
String s1 = new String(“abc”);这句话创建了几个字符串对象?
会创建一个或者两个对象:
- 字符串常量池中不存在 “abc”:会创建 2 个 字符串对象。一个在字符串常量池中,由
ldc
指令触发创建。一个在堆中,由new String()
创建,并使用常量池中的 “abc” 进行初始化。 - 字符串常量池中已存在 “abc”:会创建 1 个 字符串对象。该对象在堆中,由
new String()
创建,并使用常量池中的 “abc” 进行初始化。
异常
Java异常类层次结构图:
Java的异常体系主要基于两大类:Throwable类及其子类。Throwable有两个重要的子类:Error和Exception,它们分别代表了不同类型的异常情况。
error
(错误):表示运行时环境的错误。错误是程序无法处理的严重问题,如系统崩溃、虚拟机错误、动态链接失败等。通常,程序不应该尝试捕获这类错误。例如,OutOfMemoryError
、StackOverflowError
等。Exception
(异常):表示程序本身可以处理的异常条件。异常分为两大类:- 非运行时异常:这类异常在编译时期就必须被捕获或者声明抛出。它们通常是外部错误,如文件不存在(
FileNotFoundException
)、类未找到(ClassNotFoundException
)等。非运行时异常强制程序员处理这些可能出现的问题,增强了程序的健壮性。 - 运行时异常:这类异常包括运行时异常(
RuntimeException
)和错误(Error
)。运行时异常由程序错误导致,如空指针访问(NullPointerException
)、数组越界(ArrayIndexOutOfBoundsException
)等。运行时异常是不需要在编译时强制捕获或声明的。
- 非运行时异常:这类异常在编译时期就必须被捕获或者声明抛出。它们通常是外部错误,如文件不存在(
try-catch-finally 的使用?
try
块:用于捕获异常。其后可接零个或多个catch
块,如果没有catch
块,则必须跟一个finally
块。catch
块:用于处理try
捕获到的异常。finally
块:无论是否捕获或处理异常,finally
块里的语句都会被执行。当在try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行。例如:try{return “a”} fianlly{return “b”},最终会返回“b”。但是,如果在try
{…}catch
{…}中程序所在线程死亡,或者JVM提前终止,finally
块中的语句不会执行。
泛型
什么是泛型?
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。例如:
List<String> list = new ArrayList<String>();
// list中只能放String, 不能放其它类型的元素
反射
反射赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。反射可以让我们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。
但是,反射让我们在运行时有了分析操作类的能力的同时,也增加了安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。
反射的应用场景?
像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。但是!这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。
另外,像 Java 中的一大利器 注解 的实现也用到了反射。
注解
Annotation
(注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
注解本质是一个继承了Annotation
的特殊接口,JDK 提供了很多内置的注解(比如 @Override
、@Deprecated
),同时,我们还可以自定义注解。
SPI
SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
SPI和API有什么区别?
一般模块之间都是通过接口进行通讯,因此我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。
- 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。
- 当接口存在于调用方这边时,这就是 SPI 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。
举个例子:用户去厂家买车,会挑选各种各样的车型,这些用户不需要知道这些车是怎么制造的,只需要看商家有什么车型,直接购买就可以了,这就是API;而SPI就好比商家(服务提供者)找的代工厂,提供好了制造标准,你可以随意发挥自己的优势,随意改变车辆的颜色外观,但是你必须按照我定义的标准去制造车辆。
序列化
什么是序列化?什么是反序列化?
- 序列化:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式
- 反序列化:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程
对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class)
序列化和反序列化常见应用场景
- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
I/O
IO 即 Input/Output
,输入和输出。
BIO、NIO、AIO区别是什么?
- BIO(blocking IO):就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。优点是代码比较简单、直观;缺点是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
- NIO(non-blocking IO) :Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
- AIO(Asynchronous IO) :是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
NIO
NIO是一种同步非阻塞的IO模型,同步是指线程不断轮询IO事件是否就绪,非阻塞是指线程在等待IO的时候,可以同时做其他任务。
同步的核心就Selector(I/O多路复用),Selector代替了线程本身轮询IO事件,避免了阻塞同时减少了不必要的线程消耗;非阻塞的核心就是通道和缓冲区,当IO事件就绪时,可以通过写到缓冲区,保证IO的成功,而无需线程阻塞式地等待。