【-------------数据类型------------】
一、基本类型、包装类型
基本类型:整型(byte short int long )浮点型(float double)布尔型(boolean) 字符型(char)
包装类型:Byte Short Integer Long Float Double Boolean Character
1.基本类型的转换方式
自动类型转换(小–>大): byte --> short --> int --> long --> float --> double
强制类型转换(大–>小): 小类型 变量名 = (大类型) 值
2.为什么需要包装类型
Java是面向对象的,很多地方要求使用对象而不是基本数据类型,比如集合类中,无法存放基本数据类型
3.基本类型和包装类型的区别
(1)默认值:基本类型默认值为0,包装类型默认值为null
(2)初始化:基本类型不需要new,包装类型需要
(3)存储:基本类型存储在栈上,占用固定的内存;包装类型存储在堆上,占用更多的内存
4.自动拆装箱:基包转换
装箱:基本类型——>包装类型;拆箱:包装类型——>基本类型
装箱其实就是调用了包装类的 valueOf() 方法(Integer.valueOf()),拆箱其实就是调用了 xxxValue() 方法(Integer.intValue())
大小比较:包装类先拆箱成基本类型,然后再进行比较
二、引用类型(类、数组、接口)
1.String
String是一个类,所以是引用数据类型,因为String被final修饰,所以不能被继承。
(1)String/StringBuffer/StringBuilder的区别
【可变性】String内部的value值是final修饰的,所以是一个不可变的类,每一次修改String值时,都会产生一个新的对象;StringBuffer和StringBuilder是可变类,字符串的变更不会产生新的对象。
【线程安全】String是一个不可变的类,所以线程安全,StringBuffer也是线程安全的,因为它的每个操作方法中都用了synchronized同步关键字,StringBuilder不是线程安全的,所以在多线程环境下对字符串操作时应该使用StringBuffer。
【性能】String是性能最低的,因为在字符串拼接或者修改时,需要创建新的对象,StringBuilder性能更高,因为StringBuffer加了同步锁。
【存储】String存储在常量池中,后两者存储在堆内存空间中。
(2)String为什么是不可变的
1.保存字符串的数组被final修饰且为私有的,并且string类没有提供/暴露修改这个字符串的方法。
2.String类被final修饰导致其不能被继承,进而避免了子类破坏String的不可变。
(3)String创建的对象数量
String str = new String ("hello");创建了几个对象?
创建的对象数是1个或2个。new String 会先去常量池中判断有没有此字符串,如果有则只在堆上创建一个字符串并且指向常量池中的字符串,如果常量池中没有此字符串,则会创建 2 个对象,先在常量池中新建此字符串,然后把此引用返回给堆上的对象。
(4)字符串拼接如何实现
2.接口和抽象类的区别
抽象类属于模板设计,不能被实例化(不能使用 new 关键字创建实例),只能被继承。抽象类可以包含抽象方法和具体方法的定义,用于作为其他类的父类。
//动物类可以作为抽象类,因为动物不是具体的,需要通过子类比如猫类、狗类继承
abstract class Animal {
//成员变量:可以拥有各种访问修饰符
static String name;
//构造函数
public Animal() {
}
//抽象方法:以分号结尾,无方法体,必须在子类中实现重写
abstract void eat();
//具体方法:可以拥有各种访问修饰符
public void play() {
System.out.println("玩耍");
}
}
接口是一组抽象方法的集合。接口定义了对象应该具备的行为,类可以实现接口来达到多继承的效果。
public interface Demo{
//常量:必须初始化
public static final String s = "我是接口";
//接口属于实现,不是继承,所以没有构造方法
//抽象方法:必须实现,默认修饰 public abstract
public abstract void play();
//静态方法:可以使用接口名调用静态方法
static void test(){
system.out.println("我是静态方法");
}
//默认方法:为了扩展性和复用性
default void test2(){
system.out.println("我是默认方法");
}
}
(1)定义方式
(1)抽象类:使用关键字“abstract”来定义
(2)接口:使用关键字“interface”来定义
(2)继承和实现
(1)抽象类:使用关键字“extends”来继承,一个类只能继承一个抽象类
(2)接口:使用关键字“implements”来实现,一个类可以实现多个接口
(3)成员变量和构造函数
(1)抽象类:拥有成员变量和构造方法(初始化抽象类的成员变量),可以拥有各种访问修饰符
(2)接口:不能拥有成员变量(只能包含常量,默认public static final修饰)和构造方法
(4)方法实现
(1)抽象类:可以包含抽象方法(没有实现的方法,需要在子类中被重写)和具体方法
(2)接口:包含抽象方法(类实现接口时,必须实现接口中定义的所有抽象方法)、静态方法(静态方法是属于接口本身的方法,它可以直接通过接口名调用,而不需要创建接口的实例)和默认方法(默认方法是指在接口中定义具体实现的方法。当接口的实现类没有实现该方法时,就会使用接口中默认的方法实现)
3.类之间的关系
【-------------面向对象------------】
一、面向对象和面向过程的区别
面向对象:如果需要实现某个功能,需要先创建对应的对象,然后让对象执行行为。特点是降低系统的耦合,更容易扩展和维护。适用于大规模问题
面向过程:如果需要实现某个功能,只需要编写对应的函数。特点是性能高,但不易拓展和维护。适用于小规模问题
二、面向对象的三大特征
1.封装:将属性和方法写到抽象的类中,外界使用类创建对象,隐藏实现细节,提供公共的访问方式,提高代码安全性和复用性
2.继承:子类默认继承父类的所有属性和方法,子类可以重写(override)父类的属性和方法,子类可以拥有多个父类
3.多态:不同的子类对象调用相同的父类方法,产生不同的执行结果,是子类不同的实现,增加代码的灵活度
// 父类Parent里有say()方法,两个子类重写了say()
// p1和p2都是父类的实例,但是调用的say()是不同的实现
Parent p1 = new Son();
Parent p2 = new Daughter();
p1.say();
p2.say();
三、重写、重载的区别
是Java多态性的不同表现,重写是父类与子类之间多态性的表现,重载是一个类中多态性的表现
1.重写(override):子类和父类的方法名称、参数列表都相同,子类对方法体进行修改
【运行时多态】在程序运行时,根据实际对象的类型来确定调用的方法。
2.重载(overload):在一个类中可以有多个同名方法,但必须有不同的参数列表(参数类型、个数或顺序不同)
【编译时多态】在编译时,根据调用时提供的参数类型和数量来确定具体调用的方法。
四、构造方法为不能被重写、能被重载
作用:在创建对象时初始化对象的数据成员,在new对象时被自动调用,构造函数是没有返回值的
与类名相同,没有返回类型,可以有参数列表
如果没有构造方法,Java编译器会自动提供一个默认的构造方法,不带任何参数也不执行任何操作
构造方法不可以被继承,因此不能重写,但能重载:
重写是子类方法重写父类的方法,重写的方法名不变,而类的构造方法名必须与类名一致,假设父类的构造方法如果能够被子类重写则子类类名必须与父类类名一致才行;而重载是针对同一个方法名的,所以Java构造方法可以被重载。
五、深拷贝和浅拷贝的区别
1.深拷贝:复制并创建一个一模一样的对象,不共享内存,修改新对象,旧对象保持不变
2.浅拷贝:只复制指向某个对象的指针,而不复制对象本身,新旧对象共享一块内存
七、JAVA对象的创建过程
- 检查类是否加载(非必然步骤,如果没有就执行类的加载);
- 分配内存;
- 初始化零值;
- 设置头对象;
- 执行<init>方法(该方法由实例成员变量声明、实例初始化块和构造方法组成)。
八、JAVA创建对象的几种方式
1.使用new关键字
通过调用类的构造函数来创建对象
public class MyClass {
int x;
MyClass(int x) {
this.x = x;
}
public static void main(String[] args) {
MyClass obj = new MyClass(10); // 使用new关键字创建对象
System.out.println(obj.x); // 输出: 10
}
}
2.使用反射
Java的反射API允许在运行时创建对象。使用Class类的newInstance方法
public class MyClass {
// ... 省略其他代码 ...
public static void main(String[] args) throws Exception {
Class<?> clazz = MyClass.class;
MyClass obj = (MyClass) clazz.getDeclaredConstructor(int.class).newInstance(20); // 使用反射创建对象
System.out.println(obj.x); // 输出: 20
}
}
3.使用克隆
public class MyClass implements Cloneable {
int x;
// ... 省略其他代码 ...
@Override
protected MyClass clone() throws CloneNotSupportedException {
return (MyClass) super.clone(); // 使用clone方法创建对象的副本
}
public static void main(String[] args) throws CloneNotSupportedException {
MyClass original = new MyClass(40);
MyClass copy = original.clone(); // 使用clone方法创建对象的副本
System.out.println(copy.x); // 输出: 40
}
}
4.使用反序列化
import java.io.*;
public class MyClass implements Serializable {
// ... 省略其他代码和序列化ID ...
public static void main(String[] args) throws Exception {
// 假设我们有一个包含MyClass对象的文件
FileInputStream fis = new FileInputStream("myfile.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
MyClass obj = (MyClass) ois.readObject(); // 通过反序列化创建对象
ois.close();
fis.close();
// 现在你可以使用obj了
}
}
九、反射
在程序运行期间,动态获取对象的属性和方法
应用场景:Spring/Spring Boot、MyBatis 等等框架中都⼤量使⽤了反射机制。这些框架中也⼤量使⽤了动态代理,⽽动态代理的实现也依赖反射。
【-------------关键字------------】
一、Java访问权限
1.public:可以被任意类访问,可修饰类、成员变量、方法、内部类
2.protected:对同一包内的类和所有子类可见
3.default:对同一包内的类可见
4.private:只能在本类中访问
二、static、final区别
1.static
静态成员属于类,而不是属于类的实例,可以被类直接访问,而不需要创建类的实例
(1)声明静态变量:所有实例共享相同的静态变量
(2)声明静态方法:可以通过类名调用,而不需要创建类的实例
(3)声明静态代码块:在类加载时执行,并且只执行⼀次
(4)声明静态内部类:
2.final
final 用于修饰变量、方法或类时,表示它是不可变的。对于变量,⼀旦赋值后就不能再修改;对于方法,表示方法不能被子类重写;对于类,表示类不能被继承。
三、final/finally/finalize区别
四、super、this区别
1.super:子类调用父类的方法、属性、构造方法时,放在程序首行
2.this:调用本类中的方法、属性、构造方法时,放在程序首行
五、==、equals()、hashCode()
1.==和equals()区别
都是用来判断两个数据是否相等
(1)来源不同
==是运算符;equals来自于Object类定义的一个方法
(2)使用范围不同
==可以用于基本数据类型(比较值本身)和引用类型(比较对象的内存地址,是否指向了同一个对象);equals只能用于引用类型(重写之前,等价于==比较内存地址,重写之后,比较对象的内容)
2.hashCode()和equals()的关系
3.为什么要重写hashCode()和equals()
六、&和&&区别
1.&&运算符在运算时当第一个表达式的值为false时,就不会再计算第二个表达式;而&运算符则不管第一个表达式的值是否为真都会执行两个表达式。
2.&运算符可以用作位运算符,而&&运算符不可以。
【-------------异常------------】
一、什么是异常
异常是JAVA中一种表示程序出现问题或错误的事件,通常在程序执行期间发生,并且可能会导致程序终止或产生意外结果
比如试图访问一个不存在的数组索引,会抛出ArrayIndexOutOfBoundsException异常
二、异常的体系
通过异常类的对象来表示异常,所有的异常类都是Throwable类的子类,Throwable又分为Error和Exception,Error是系统内部错误,比如虚拟机异常,是程序无法处理的,Exception是程序问题,又分为:
(1)可检查异常:在编译时强制要求处理的异常,否则编译器会报错,比如IOException
(2)不可检查异常:由运行时错误引起的,比如NullPointerException
三、异常处理的方法
(1)try-catch块:在try块中,放置可能引发异常的代码,在catch块中,处理异常,比如输出错误信息或进行补救措施;还可以使用finally块,包含的代码无论是否发生异常都会被执行,用于释放资源或进行清理操作
(2)throw/throws关键字:通过throw主动抛出异常;通过throws将异常传递给调用者处理,用于在方法签名中声明可能抛出的异常
四、项目中处理异常的最佳实践
使用try-catch-finally来捕获和处理异常,保证程序的稳定和可靠
自定义异常类来更好的反应业务逻辑,避免过多的嵌套异常,简化调试过程
【-------------集合/容器------------】
一、集合框架
Java 的集合框架是由一组接口和类组成的,这些接口和类之间形成了一个层次结构。
1.最上层的接口
前三个由Collection接口派生而出,之所以定义多个接口,是为了以不同的方式操作集合对象。Collection⼀次存⼀个元素,是单列集合,Map⼀次存⼀对元素,是双列集合。
-
List(列表):有序的集合,可重复
-
Set(集):无序的集合,不重复
-
Queue(队列):按照FIFO(先进先出)原则存储数据,可重复
-
Map(映射):存储键/值对的集合,键必须唯一
2.中间的抽象类
3.最后的实现类
Java集合类,都是上述接口的实现类
import java.util.ArrayList;
import java.util.HashSet;
import java.util.HashMap;
import java.util.LinkedList;
public class CollectionExample {
public static void main(String[] args) {
// List示例
ArrayList<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Cherry");
// Set示例
HashSet<String> set = new HashSet<>();
set.add("Apple");
set.add("Banana");
set.add("Cherry"); // 这里会去重
// Map示例
HashMap<String, Integer> map = new HashMap<>();
map.put("Apple", 1);
map.put("Banana", 2);
map.put("Cherry", 3);
// Queue示例
LinkedList<String> queue = new LinkedList<>();
queue.offer("Apple");
queue.offer("Banana");
queue.offer("Cherry");
}
}
二、ArrayList和LinkedList区别
ArrayList基于数组实现,适合查询,不适合增删,需要扩容
LinkedList基于双向链表实现,适合增删,不适合查询,不需要扩容
三、泛型
集合存在⼀个缺点,即当将对象添加到集合中后,集合会失去对该对象的具体数据类型的记忆,导致在 取出对象时,集合将其视为 Object 类型。这是因为集合的设计者在创建时⽆法确定集合将被⽤来存储哪种类型的对象,因此选择设计成能够存储任何类型的对象,以保持通⽤性。缺点如下:
1.将不同类型的对象添加到集合中,引发异常
2.取出对象后通常需要进行强制类型转换
泛型可以传递多种类型的数据,但是当类型确定时不能传递其他类型的数据。例如List<T>可以为任意类型,List<String> 表示该列表只能保存字符串类型的对象。有泛型类、泛型接口和泛型方法。
泛型擦除:为了兼容早期没有泛型的版本,JAVA编译器在编译时会移除所有的泛型信息
【-------------IO------------】
一、分类
1.按流向分类:I/O 流分为输入流和输出流
2.按单位分类:I/O 流分为字节流和字符流。字节流用于处理原始的⼆进制数据,字符流用于处理文本数据
【-------------多线程------------】
一、 线程和进程
线程是程序执行的最小单位,是进程中的一个执行流程
进程是正在运行的程序的实例,包含了程序代码、数据和资源的集合
二、创建线程的方式
1.继承Thread类
重写的是run()方法,而不是start()方法,但是占用了继承的名额,Java中的类是单继承的
2.实现Runnable接口
实现Runnable接口,实现run()方法,使用依然要用到Thread,这种方式更常用
3.实现Callable接口
实现Callable接口,实现call()方法,得使用Thread+ FutureTask配合,这种方式支持拿到异步执行任务的结果
4.使用线程池
实现Callable接口或者Runnable接口都可以,由ExecutorService来创建线程
【-------------JVM------------】
一、JVM、JDK和JRE的关系
JDK是JAVA语言的软件开发工具包,提供了JAVA的开发环境和运行环境
JRE是JAVA的运行环境,是可以在其上运行、测试和传输应用程序的JAVA平台
JVM是JAVA虚拟机,是JAVA跨平台的核心,在不同平台上运行时不需要重新编译
简单来说,JDK=JRE+各种开发工具;JRE=JVM+各种类库
二、堆(Heap)和栈(Stack)的区别
1.概念
栈是一个用于存储方法调用和局部变量的地方。管理方式是后进先出(先分配的内存后释放)。栈的空间相对较小,分配和释放由系统自动管理,因此效率很高,但生命周期也短,方法执行结束后,栈中的数据会被自动清除。
堆用于动态分配对象的内存空间,主要用于存储通过new关键字创建的对象和数组。对象的生命周期由垃圾回收器负责管理,堆的分配速度相对较慢,可以存储较大的对象,对象的生命周期可以很长。
2.使用场景
变量的生命周期局限在函数内部,就是局部变量,占据的内存区域就是栈区
变量的生命周期希望自己控制,占据的内存区域就是堆区
3.性能
在栈区的性能更高,本质是指针的移动
进程中的所有线程共享一个堆区,因此堆区内存分配器必须处理好线程安全问题
4.内存大小
栈区的大小是固定的,并且容量有限,否则会出现栈溢出
堆区容量更大,必须确保内存可以释放掉,否则会出现内存泄漏
三、垃圾回收机制
1.为什么需要垃圾回收
当程序员手动进行内存管理时,会存在一些问题:内存泄露(忘记释放内存空间)、悬垂指针(忘记初始化指向已经回收的内存地址的指针)、错误释放引发 BUG
2.如何找到垃圾
JVM对内存自动化回收,采用垃圾回收器技术(GC)。采用根可达性算法判定对象是否存活:
(1)枚举一系列对象作为GC Roots,为垃圾回收器提供一个初始的扫描位置
(2)从GC Roots出发,搜索引用链,某个对象不可达时,该对象判定为不可能再被使用(死亡)
3.垃圾回收算法
垃圾回收算法:垃圾回收的策略,用于确定哪些对象应该被回收
(1)标记清除:标记阶段是把所有活动对象都做上标记的阶段。清除阶段是把那些没有标记的对象,也就是非活动对象回收的阶段。优点是实现简单、速度快,缺点是会造成内存碎片(没有一块连续的内存可以存放大的对象)
(2)标记复制:标记阶段是把所有活动对象都做上标记的阶段。把活动对象复制到另一半空间,把原空间里的所有对象都回收掉,变成空闲区域。优点是效率高、不会产生内存碎片,缺点是会有一半的内存被浪费掉
(3)标记压缩:标记阶段是把所有活动对象都做上标记的阶段。将打上标记的活动对象复制到堆的开头。压缩阶段并不会改变对象的排列顺序,只是缩小了它们之间的空隙, 把它们聚集到了堆的一端。缺点是时间消耗长
4.垃圾回收器
在以上算法的基础上,构建了十种垃圾回收器。垃圾回收器负责实际回收内存
Serial、CMS用了标记清除算法;ParNew用了标记复制算法;CMS、G1用了标记压缩算法
5.堆内存逻辑分区
新生代,存储新的对象;老年代:存储经过多次回收但回收不掉的对象
先存到eden区中,垃圾回收之后,将活的对象复制到survivor区中,清空eden区;如果再次添加新对象到eden区,此时垃圾回收要考虑survivor区,将两个区活的对象复制到第二个survivor区中,清空eden和第一个survivor区;如果再次添加新对象到eden区,此时垃圾回收要考虑第二个survivor区,将两个区活的对象复制到第一个survivor区中,清空eden和第二个survivor区。
四、类加载机制
1.类加载
把描述类的数据.class文件从磁盘加载到内存,并对字节码中的数据进行校验、转换解析和初始化,最终形成可以被虚拟机JVM直接使用的类型。
2.类的生命周期/加载阶段
2.类加载器
在类加载流程第⼀步“加载”阶段,通过⼀个类的全限定名来获取描述该类的二进制字节流,实现这⼀功能的代码叫做类加载器。
有启动类加载器、扩展类加载器、应用类加载器、自定义类加载器
3.双亲委派模型
【-------------新特性------------】
一、Lambda 表达式
允许将一个函数作为另一个函数的参数,也就是代码的传递,不能独立执行,必须实现函数式接口,并且返回一个函数式接口的对象。
函数式接口:仅包含一个抽象方法的接口