Java基础
- Java基础
- 一. 数据类型
- 二. 面向对象
- 三. Object
- 四. String
- 1. String , StringBuffer 和 StringBuilder的区别是什么? String为什么是不可变的?⭐️⭐️⭐️⭐️⭐️
- 2. String的equals()方法?⭐️⭐️⭐️⭐️⭐️
- 3. 字符串拼接用“+”还是StringBuilder?
- 4. String的equals() 和 Object的equals()有何区别?
- 5. 了解字符串常量池吗?(放到JVM中了)
- 6. String s1 = new String("abc");这句话创建了几个字符串对象?
- 7. String中的intern方法有什么作用?
- 8. 重载和重写的区别?⭐️⭐️⭐️⭐️⭐️
- 9. 内部类了解吗?匿名内部类了解吗?⭐️⭐️⭐️
- 五. 反射
- 六. 注解
- 七. 泛型
- 八. IO流
- 其他(打字打累了😭😡 🤬 这里开始截屏+思路)
- 额,暂时没整理全,但是先放这一部分
!!!!本文是基于javaGuide开源项目进行学习的记录和自我总结!!!!
Java基础
一. 数据类型
1. Java有哪些基础数据类型? 它们的默认值和占用空间大小知道不? 说说这些基础数据类型对应的包装类型?⭐️⭐️⭐️⭐️
-
8中基本数据类型:
byte
,short
,int
,long
,float
,double
,char
,boolean
-
默认值和占用空间大小
基本类型 | 位数bit | 字节Byte | 默认值 | 取值范围 |
---|---|---|---|---|
byte | 8 | 1 | 0 | -127 ~ 128 |
short | 16 | 2 | 0 | -2^15 ~ 2^15-1 |
int | 32 | 4 | 0 | -2^32 ~ 2^32-1 |
long | 64 | 8 | 0L | -2^63 ~ 2^63-1 |
float | 32 | 4 | 0f | |
double | 64 | 8 | 0d | |
char | 16 | 2 | ‘uoooo’ | |
boolean | 1 | false | true , false |
- 对应的包装类型
基本类型 | 包装类型 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
2. 基本类型和包装类型的区别?
- 用途
除了定义一些常量和局部变量之外 , 我们在其他地方比如方法参数 , 对象属性中很少会使用基本类型来定义变量 . 并且 , 包装类型可用于泛型 , 而基本类型不可以
- 存储方式
- 基本数据类型 :
局部变量 存放在Java虚拟机栈中的局部变量表中
成员变量(未被static
修饰) 存放在Java虚拟机的堆中- 包装类型
包装类型属于对象类型 , 我们知道几乎所有对象实例都存在于堆中
- 占用空间
相比于包装类型(对象类型) , 基本数据类型占用的空间往往非常小
- 默认值
成员变量包装类型不赋值就是
null
, 而基本类型有默认值且不是null
- 比较方式
- 基本数据类型 :
==
比较的是值
包装类型 : 所有整型包装类对象之间值的比较 , 全部使用equals()
方法 .==
比较的是内存地址 ;
3. 包装类型的常量池技术了解吗? ⭐️⭐️⭐️⭐️⭐️
- Java基本类型的包装类的大部分都实现了常量池技术 , 除了Float和Double
//我对与常量池的简单理解
常量池相当于一个池子 , 包装类型中除了float和double , 都有实现常量池技术 ,
然后这个池子有一个范围 , 如果我定义的常量属于这个范围 , 那么它们都属于常量池中这个数 ,
也就是说 , 当我再次创建一个 在这个池子范围内的值 的时候 , 可以直接用这个值对应池子中的对象 , 而不用再次创建新的对象
两次都指向同一个对象(内存地址相同) , 所以判断"=="会输出true.
而如果超出这个范围,就会创建新的对象 , 所以'=='输出为false
Integer的缓存源码
public static Integer valueOf(int i) {
//如果i在缓存池的范围内
if (i >= IntegerCache.low && i <= IntegerCache.high)
//通过 i+(-IntegerCache.low) 计算出在缓存数组中的正确索引 , 然后返回该索引处的Integer对象
return IntegerCache.cache[i + (-IntegerCache.low)];
//如果i不在缓存范围内 , 会创建一个新的Integer对象并返回
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static {
// high value may be configured by property
int h = 127;
}
}
详解
举个例子
Integer a1 = 33; Integer a2 = 33; System.out.println(a1 == a2); //输出true 在缓存池范围内,直接返回同一个内存地址 Float a11 = 333f; Float a22 = 333f; System.out.println(a11 == a22); //输出false 因为Float没有实现常量池技术 Double a3 = 1.2; Double a4 = 1.2; System.out.println(a3 == a4); //输出false 因为Double没有实现常量池技术
//来看另一个例子
Integer a1 = 40; //直接使用常量池中的对象 Integer a2 = new Integer(40); //直接创建新的对象 System.out.println(a1 == a2); //输出:false
// 为什么是false?
Integer a1 = 40
这行代码会发生装箱 , 也就是说这行代码等价于Integer a1 = >Integer.valueOf(40)
因此 ,a1
直接使用的是常量池中的对象 ,
而Integer a2 = >new Integer(40)
则会直接创建新的对象
4. 自动装箱与拆箱了解吗? 原理是什么?⭐️⭐️⭐️⭐️⭐️
- 什么是自动拆装箱?
- 自动拆装箱 其实就是 基本类型和包装类型的互转
- 装箱 : 将基本类型用它们对应的引用类型包装起来
- 拆箱 : 将包装类型转换为基本数据类型
简单理解: 装箱就是把基本的打包一下 , 装起来 ; 拆箱就是把包装拆了
//举例
Integer i = 10; //装箱 等价于 Integer i = Integer.valueOf(10)
int n = i; //拆箱 等价于 int n = i.intValue();
//字节码文件内容
L1
LINENUMBER 8 L1
ALOAD 0
BIPUSH 10
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
PUTFIELD AutoBoxTest.i : Ljava/lang/Integer;
L2
LINENUMBER 9 L2
ALOAD 0
ALOAD 0
GETFIELD AutoBoxTest.i : Ljava/lang/Integer;
INVOKEVIRTUAL java/lang/Integer.intValue ()I
PUTFIELD AutoBoxTest.n : I
RETURN
- 原理
从字节码中 , 我们发现
装箱其实就是调用了 包装类的valueOf()
方法 ,
拆箱其实就是调用了xxxValue()
方法
注 : 如果频繁拆装箱的话 , 也会严重影响系统的性能 . 我们应该尽量避免不必要的拆装箱操作
5. 遇到过自动拆箱引发的NPE问题吗?⭐️⭐️⭐️⭐️
//NPE问题举例 :
- 数据库的查询结果可能是null , 因为自动拆箱 , 用基本数据类型接受有NPE风险
public class AutoBoxTest{
@Test
void should_Throw_NullPointerExce(){
long id = getNum(); //自动拆箱-->但返回的是null,null不能正常拆箱-->会发生NPE问题
}
//返回的是包装类型
public Long getNum(){
return null;
}
}
- 三元运算符使用不当会导致诡异的NPE异常
public class Main{
public static void main(String[] args){
Integer i = null;
Boolean flag = false;
System.out.println(flag ? 0 : i);
}
}
详解:
System.out.println(flag ? 0 : i);
flag ? 0 : i
三元运算符的意思是 : 如果flag为true , 则结果是0 ; 如果flag为false , 则结果是i- 关键在于i是一个Integer对象 , 而sout方法期望一个可以自动拆箱为int类型的值
- 当flag为false时 , 三元运算符选择i作为结果 ; 而sout方法内部会尝试将i自动拆箱为int类型的值
- 但是 , 这里的i = null , 而null不能被正确拆箱 , 所以会报NPE的问题
6. 为什么浮点数运算时会有精度丢失的风险?怎么解决?
- 为什么?
浮点数的长度是有限的,需要做一些取舍,所以会导致精度缺失
- 怎么解决?
用
BigDecimal
7. 超过long整型的数据应如何表示?
BigInteger
表示
BigInteger
内部使用int[]
数组来存储任意大小的整形数据
二. 面向对象
1. 面向过程编程(POP)和面向对象编程(OOP)
- 区别 : 解决问题的方式不同
- 面向过程编程(POP) : 面向过程把解决问题的过程拆成一个个方法 , 通过一个个方法的执行解决问题
就是把一个项目 , 一件事按照一定的顺序 , 从头到尾一步一步地做下去 , 先做什么 , 后做什么 , 一直到结束
- 面向对象编程(OOP) : 面向对象会先抽象出对象 , 然后用对象执行方法的方式解决问题
就是把一个项目 , 一件事分成更小的项目 , 或者说分成一个个更小的部分 , 每一部分负责什么方面的功能 , 最后再由这些部分组合而成为一个整体
- OOP的优点
- 易维护 : 由于良好的结构和封装性 , OOP程序通常更容易维护
- 易复用 : 通过继承和多态 , OOP设计使得代码更具有复用性 , 方便扩展功能
- 易扩展 : 模块化设计使得系统扩展变得更加容易和灵活
- 注意
- POP和OOP的性能差异主要取决于它们的运行机制,而不仅仅是编程范式本身。
- 有人说:面向过程性能比面向对象高。因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发。(比如操作系统要用c或c++进行开发)
----》这并不是根本原因。- 面向过程也需要分配内存,计算内存偏移量。Java性能差是因为它是半编译语言,最终的执行代码并不是可以直接被CPU执行的二进制机械码。
- 而面向过程语言大多数都是直接编译成机械码在电脑上执行,并且其他一些面向过程的脚本语言性能也不一定比Java好
3. 创建一个对象用什么运算符?对象实体与对象引用有何不同?
//对象实体就是实例 --》存在堆中
//对象引用就是引用变量(也可以理解为指针)–》引用变量(创建引用对象实际上是创建了一个局部变量)–》存放在栈中
//举个例子
class MyClass {
int data;
void show() {
System.out.println("Showing data: " + data);
}
}
public class Main {
public static void main(String[] args) {
MyClass obj1 = new MyClass(); // 创建对象实例
MyClass obj2 = obj1; // obj2 是 obj1 的引用
obj1.data = 10;
obj2.show(); // 将输出 "Showing data: 10",因为 obj2 引用了 obj1 的实例
}
}
- 创建对象
用
new
运算符,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)
- 对象实体和对象引用有何不同?
- 一个引用变量(对象引用)可以指向 0个 / 1个 对象
- 一个对象 可以有n个引用指向它
- 也就是说,一个引用要么不指向任何对象,要么指向一个对象(引用只能唯一 。 就像身份证号一样,起到一个定位的作用)
- 而一个对象可以由多个引用指过来(虽然例子不太恰当,但是如果有人办假证,那么这么多身份证号都可以指向同一个人)
4. 对象相等和引用相等的区别?
- 对象相等一般比较的内存中存放的内容是否相等
- 引用相等一般比较的是它们指向的内存地址是否相等
- 举个例子
String str1 = "hello";
String str2 = new String("hello");
String str3 = "hello";
// 使用 == 比较字符串的引用相等
System.out.println(str1 == str2); //false str1和str2是两个对象,所以内存地址(引用)肯定不同
System.out.println(str1 == str3);//true str1和str3 因为字符串常量池的存在--》值相同时,引用指向同一个对象
// 使用 equals 方法比较字符串的相等
System.out.println(str1.equals(str2));//true 俩对象值相等
System.out.println(str1.equals(str3));//true 俩对象值相等
5.如果一个类没有声明构造方法,该程序能正确执行吗?
构造方法是一个特殊的方法,主要作用是完成对象的初始化工作
- 没有声明构造方法也能执行,java有默认的无参构造方法。
- 当我们自己添加了类的构造方法,Java就不会添加无参构造了。
6. 构造方法有哪些特点?是否可被override?
- 构造方法特点:
- 名称与类型相同:构造方法名称必须与类名完全一致
- 没有返回值:构造方法没有返回类型,且不能使用
void
声明- 自动执行:在生成类的对象时,构造方法
- 构造方法不能被重写(override),但可以被重载(overload)。
- 一个类中可以有多个构造方法,这些构造方法可以具有不同的参数列表,以提供不同的对象初始化方式。
7.面向对象三大特征
- 封装
封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部(private修饰),不允许外部对象直接访问,但是可以提供一些方法(public)供外部来操作属性
- 继承
- 不同类型的对象,相互之间经常有一定数量的共同点。
- 继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。
//注意
- 子类拥有父类的所有属性和方法,但是父类中的私有属性和方法子类无法访问,只是拥有
2.子类可对父类进行扩展,即可以定义自己的属性和方法- 子类可以重写父类的方法
- 多态
///举个例子
class Animal {
void makeSound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Bark");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Meow");
}
}
public class Main {
public static void main(String[] args) {
Animal myPet = new Dog(); // 父类引用指向子类对象
myPet.makeSound(); // 输出 "Bark"
Animal anotherPet = new Cat(); // 父类引用指向另一个子类对象
anotherPet.makeSound(); // 输出 "Meow"
}
}
//详述
- 多态
多态就是可以让一个对象在不同条件下表现为不同的形式,具体表现为 父类的引用指向子类的对象- 多态的前提(对上面代码进行分析)
- 继承与实现:在多态中必须存在有继承或实现关系的子类和父类
`class Dog extends Animal` //Dog类继承了Animal类 /* 必须要在子类继承父类的条件下, 是因为 继承本质上是为了扩展父类的功能, 而多态要在这样的前提下,让父类也能够使用子类扩展的这些东西 */
- 方法的重写:子类重写了父类的某些方法(@Override)
//重写了父类的makeSound方法 @Override void makeSound() { System.out.println("Bark"); } /* 因为子类就是为了扩展父类而存在的, 所以它可能会重写父类的一些方法或自己定义一些新方法 --》那么多态情况下,父类引用调用的是谁的方法和属性呢?下面有解释 */
- 父类引用指向子类对象
//父类引用:Aniaml myPet;子类对象(实例)new Dog(); Animal myPet = new Dog(); /* 这样做 就是为了 让父类能够调用子类扩展的东西 */
- 多态的特点
- 多态时,访问父类的成员变量
- 多态时,子父类存在同名的非静态成员方法时,访问的是子类中重写的方法
- 多态时,子父类存在同名的静态成员变量、成员方法时,访问的父类的成员函数
- 多态时,不能访问子类独有的方法
8. static和final关键字
上面很多地方都提到了static和final关键字,这里我们来简单了解一下
- static
- static修饰的成员变量和方法,是属于类的–》可以通过类名直接访问(我们见过的Math.min() ,这个就是static的使用场景之一)
- 为什么要用static?什么时候用static?(举几个简单的例子)
- 共享状态。当多个对象需要共享相同的数据时,可以使用static变量来存储这些共享状态
- 工具类方法。工具类(如Math、Collections等),我们常见的Math.max() 就是通过类名直接调用
- 单例模式等
- final
- final关键字很神奇
- final修饰变量,有两种可能:基本数据类型和引用类型。如果修饰的是基本数据类型,那基本数据类型不可变,但是如果修饰的是引用数据类型,对象引用不可变,但是!!!对象实例的内容是可以改变的
- final修饰变量,变量不可改变,且成员变量必须在定义时初始化(三种方法),局部变量必须在使用前初始化,且只能被初始化一次
1.为什么 要初始化? 因为final修饰的变量,一旦被初始化就不能被重新赋值,所以一定要在使用之前完成对他们的创建(也就是给他们赋值) 2. 为什么成员变量和局部变量初始化时机不一样? - 上面说“因为final修饰的变量,一旦被初始化就不能被重新赋值,所以要在使用之前完成初始化” - 而成员变量和局部变量初始化的时机也和他们被使用的时机有关 - 成员变量:成员变量与实例对象关联,它们在·对象创建时·就应该有确定的值 - 局部变量:局部变量与方法调用有关,他们作用域小,所以不着急初始化
成员变量初始化的三种方法 - 声明时初始化:final int value = 10; - 构造器中初始化:public MyClass(){this.value = 10;} - 实例初始化块中初始化:{ value = 10;}
- final修饰方法,方法不能被重写,但可以被继承
- final修饰类,类不能被继承
9. 接口和抽象类有什么共同点和区别?⭐️⭐️⭐️⭐️⭐️
- 共同点
- 都不能被实例化
- 都可以包含抽象方法,且抽象方法不能有方法体,必须在子类或实现类中实现
- 区别(接口比抽象类更抽象–》更纯粹)
1. 设计目的不同:
- 抽象类存在的意义就是为了被继承
- 抽象类中可以有 抽象方法,非抽象方法(有方法体的具体实现的方法),变量,构造器
- 抽象类更适用于 代码复用(类之间有很多共同的状态和行为【也就是属性和方法】)的情景
- 比如动物们的叫声不同,但是动物们都需要吃饭--》这就有了相同的行为, - 我就可以把这个行文(吃饭)定义为一个普通方法,再定义一个抽象方法 叫声,然后让子类继承这个类, - 因为抽象类就是要继承使用的,而继承机制决定了,子类可以继承父类所有的方法, - 所以子类也有和父类相同的“吃饭”方法,并且需要实现父类的抽象方法“叫声”。
- 接口更像是一种统一的规则,就像我们常用的usb接口一样
- 比如动物们叫声不同,我就可以定义一个 叫声接口类,每个动物类都可以实现这个接口,然后实现叫声方法,去定义动物类自己的叫声
//举例
//接口
// 动物叫声的接口
public interface AnimalSound {
void makeSound();
}
// 狗类实现AnimalSound接口
public class Dog implements AnimalSound {
@Override
public void makeSound() {
System.out.println("Woof woof!");
}
}
// 猫类实现AnimalSound接口
public class Cat implements AnimalSound {
@Override
public void makeSound() {
System.out.println("Meow meow!");
}
}
//抽象类
// 抽象动物类
public abstract class Animal {
private String name;
private int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
// 所有动物都有吃饭的行为
public void eat() {
System.out.println(name + " is eating.");
}
// 抽象方法,由子类实现
public abstract void makeSound();
}
// 狮子类继承Animal抽象类
public class Lion extends Animal {
public Lion(String name, int age) {
super(name, age);
}
@Override
public void makeSound() {
System.out.println(name + " roars.");
}
}
// 鸟类继承Animal抽象类
public class Bird extends Animal {
public Bird(String name, int age) {
super(name, age);
}
@Override
public void makeSound() {
System.out.println(name + " chirps.");
}
}
2. 单继承,多实现
抽象类基于继承,而继承是单继承(一个类只能继承一个类)–》但是可以搞成链式关系
接口可以多实现,一个类可以实现多个接口
3. 成员变量
- 接口中的成员变量只能是
public static final
类型的,不能被修改且必须有初始值。- 抽象类的成员变量可以有任何修饰符(public,private,protected),可以在子类中重新定义或赋值
4. 方法
- 接口:java8及以后,可以在接口中定义
default
方法和static
方法。java9起,可以包含private
方法- 抽象类就还是有抽象和非抽象方法,然后抽象方法没有方法体,必须在子类中实现(其实也可以不在直接子类中实现),但必须在子类的子类,反正就是继承链中其中一个子类中实现
5. 定义的关键字不同:接口(interface);抽象类(abstract)
//定义接口类 public interface Animal {...} //定义抽象类 public abstract class Animal {...}
9. 浅拷贝和深拷贝的区别了解吗?什么是引用拷贝?⭐️⭐️⭐️⭐️
- 概念补充
Java的数据类型中 , 除了基本类型(byte , short , int , long , float , double , char , boolean) 就是 引用类型(如 : 数组 , 字符串 , 类 , 接口 , 等)
- java将内存分为 栈 和 堆
- 对于基本数据类型而言 , 它们全都存放在栈中
- 对于引用类型而言 , 它们的引用存放在 栈 中 , 实际存储的值在 堆 中 (而栈中存放的引用 , 会指向堆中存储的值的地址)
拷贝一般分为两大类 : 引用拷贝 和 对象拷贝 , 而我们通常讲的深拷贝和浅拷贝都是对象拷贝
- 上答案
- 如果拷贝对象是引用类型
- 引用拷贝
直接创建一个新的对象(存在栈中)并拷贝对象的引用(也就是内存地址),这个新对象和拷贝对象指向同一个堆中的对象实例- 浅拷贝
只拷贝第一层,即要拷贝对象的引用,也要拷贝堆中的对象实例,但是如果这个对象里面还有其他引用类型的属性,只会复制这些引用类型的对象引用(也就是栈里的内存地址)- 深拷贝
不仅拷贝了栈里面的对象引用,也拷贝了堆中的对象实例,如果这个对象里面还有嵌套的引用类型的属性,那么也继续拷贝。拷贝出来的是完全全新的对象- 如果拷贝对象是“基本类型”——那其实没有浅拷贝和深拷贝之分,因为基本类型的值本身就存在于栈上
- 浅拷贝和深拷贝的实现方法(简述)
- 浅拷贝通过实现Cloneable接口 并 调用Object的clone方法(因为Object的clone方法默认是浅拷贝的
- 深拷贝是通过递归,序列化等方式进行对象拷贝
三. Object
1. Object中常见的方法
2. ==和equals()的区别⭐️⭐️⭐️⭐️⭐️
一般来说,
- 判断基本类型的值是否相等用
==
- 判断引用类型的值是否相等用
equals()
如果用==
判断对象是否相等–》则判断的是对象引用是否相等
- Object中的equals()方法源码
//可以看出,这只比较了对象的地址(也就是对象引用),并没有比较对象本身
public boolean equals(Object obj) {
return (this == obj);
}
需要注意的是:
- 类没有重写equals()方法:和
==
一样是比较对象引用- 类重写equals()方法:比如String类中的equals()方法,就可以比较两个字符串的值
3. hashCode()有什么用?
- 作用
hashCode()
的作用是获取哈希码(int
整数),也称散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。- 散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用了散列码!(可以快速找到所需要的对象)
//存取kv对的简单过程 - 当我输入一个kv对,这里的k会根据某种规则(哈希函数)进行计算,从而得出哈希码。 - 在哈希表中,这个哈希码对应着我输入的v。 - 而当我根据k获取v的时候,会根据相同的规则计算出哈希码,然后去到哈希表中找到v
- 补充:
Object的hashCode()方法是本地方法,也就是用C语言或C++实现的!!【我第一次知道诶】
4. 为什么要有hashcode?
- 这里写的嘎嘎好,特别明白,直接截图过来了
总结:
- 提高效率:Java的比较机制是这样的——先比较俩对象的hashcode值,如果相同再用equals()方法 进一步比较,如果不等,直接就可以判断俩对象不等了
- 这就有问题了??——那有hashcode进行比较了,为啥还要用equals?
你听过哈希冲突
吗,hashcode的值是通过哈希算法 计算出来的,如果这个哈希算法比较差,那么算出来的值分布就不好,那就有可能有重复的hashcode。这就是哈希冲突。但是这个时候两个对象并不相等,所以当hashcode相等时,需要调用equals()方法进一步判断
5. 为什么重写equals()方法 要重写hashcode?
简单来说就是要确定两个对象的是相等的
- 因为,java规定——如果equals判断相等,那么hashcode也要相等;
- 如果没有重写hashcode,那么可能会导致两个对象的equals相等,但hashcode不等
四. String
1. String , StringBuffer 和 StringBuilder的区别是什么? String为什么是不可变的?⭐️⭐️⭐️⭐️⭐️
这里从 源码 的角度进行分析
(1)String为什么是不可变的?
// 问:下面代码改变了String吗?
String s = "abc"; //将字符串"abc" 存储在字符常量池中,s引用指向常量池中的这个字符串对象 s = "afajie";
//答:没有–》如果没有修改原来的
s
,那发生什么了呢?
- 将“afajie”存储在字符串常量池中:如果常量池中已经存在一个相同的字符串“afajie”,那么会重用这个对象
- 引用s执行新的字符串对象:此时,s引用不再指向原来的“abc”字符串对象,而是指向常量池中“afajie”字符串对象。
原来的“abc”字符串对象仍然存在于常量池中,除非它没有任何引用指向它,否则不会被垃圾回收–》(什么时候进行垃圾回收,取决于JVM的垃圾回收策略)
String源码 探究( jdk8 )
- 成员变量
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
...
}
- String为什么是不可变的?
//private final char value[];
String s = "abc;
实际上是将abc存储在一个char数组中
相当于char[] c = {'a','b','c'}
- 而这个char数组被定义为
private
和final
private
使得char数组不能被外部调用final
修饰变量–》使得变量的值是不可变的`
//public final class String{…}
final
当final修饰类时,使得类不能被继承–》也就是说String类不能被子类重写覆盖–》String的不可变
(2)String , StringBuffer和StringBuilder的区别?
- 可变性
String
是不可变的,一旦创建,它的值就不能被改变(对String的任何修改都会生成一个新的String对象)
StringBuffer
是可变的,可以对StringBuffer对象进行修改,比如追加(append)或插入(insert)操作,而不需要创建新的对象
StringBuilder
是可变的,与StringBuffer类似,可以进行修改操作
- 线程安全
String
由于其不可变性,自然具有线程安全性,可以在多线程环境中安全使用
StringBuffer
是线程安全的,它的方法通常是同步的(synchronized),这意味着在多线程环境中,一次只有一个线程可以执行这些方法
StringBuilder
是线程不安全的,它的方法不是同步的,因此在单线程环境中性能更好,但在多线程环境中需要使用额外的同步控制
- 性能
String
:性能最差 由于其不可变性,每次修改都会创建一个新的对象,这可能会导致较高的内存消耗和性能开销,特别是在频繁修改字符串的情况下StringBuffer
:在单线程环境中,比String快,比StringBuilder慢。由于其是线程安全的,每次方法调用都会有同步开销StringBuilder
:最好,在单线程环境中,由于没有同步开销,通常比StringBuffer有更好的性能
- 详解
//先看一下源码
- StringBuffer
public final class StringBuffer extends AbstractStringBuilder implements Serializable, CharSequence{ ... //随便提溜出来一个方法 @Override //被synchronized修饰--》同步锁 --》保证了线程安全 public synchronized StringBuffer append(...) { ... } ... }
- StringBuilder
public final class StringBuilder extends AbstractStringBuilder implements Serializable, CharSequence{ ... @Override public StringBuilder append(...) { ... } ... }
- AbstractStringBuilder(上面那俩继承的类)
abstract class AbstractStringBuilder implements Appendable, >CharSequence { char[] value; ... }
- 我们可以看到
AbstractStringBuilder
方法中是char[] value;
并没有像String
一样被private和final修饰–》所以是可变的- 还可以从
StringBuffer
和StringBuilder
的源码中看到–》StringBuffer
中有synchronized
关键字–》同步锁–》所以即便StringBuffer是可变的,但是也是线程安全的
synchronized ['sɪŋkrənaɪzd]
2. String的equals()方法?⭐️⭐️⭐️⭐️⭐️
- String类的equals()方法详解
–》String类中的equals()方法 是已经重写过的
public boolean equals(Object anObject) {
//如果传入的对象引用 就是当前对象的引用,那么它们是同一个字符串,返回true
if (this == anObject) {
return true;
}
//如果传入的对象不是String类型的实例,返回false
//如果是String类型的实例,则继续比较
if (anObject instanceof String) {
//确认是String类型后,将对象强转为String类型
//为什么还要强转?--》传进来的对象还是Object类型,这段代码要进行String对象的比较--》需要转成String类型之后,才能调用String类的方法
String anotherString = (String)anObject;
//比较长度
int n = value.length;
//逐个字符串比较
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
3. 字符串拼接用“+”还是StringBuilder?
答: 其实本质上都是StringBuilder
- Java语言本身并不支持运算符重载,“+”和“+=”是专门为String类重载过的运算符,也是Java中仅有的两个重载过的运算符。
- 通过字节码文件可以看出,字符串对象通过“+”的字符串拼接方式,实际上是通过
StringBuilder
调用append()
方法实现的,拼接完成之后调用toString()
得到一个String
对象
- 用“+”的缺点
编译器不会创建单个
StringBuilder
以复用,会导致创建过多的StringBuilder
对象
String[] arr = {"he","llo","world"};
String s = "";
for(int i = 0; i < arr.length; i++){
s += arr[i]; //每次循环都要创建一个StringBuilder对象
}
System.out.println(s);
- 如果直接用
StringBuilder
对象进行字符串拼接的话,就不会存在这个问题了
String[] arr = {"he","llo","world"};
StringBuilder sb = new StringBuilder(); //只用创建一次StringBuilder对象
for(String value : arr){
sb.append(value);
}
System.out.println(sb);
4. String的equals() 和 Object的equals()有何区别?
- String中的equals方法是被重写过的,比较的是String字符串的值是否相等。
- Object的equals方法时比较的对象的内存地址
5. 了解字符串常量池吗?(放到JVM中了)
6. String s1 = new String(“abc”);这句话创建了几个字符串对象?
- 会创建1或2个字符串对象
- 如果字符串常量池中没有字符串对象“abc”的引用
- JVM会在堆内存中创建一个新的字符串对象实例(“abc”)
- 然后,JVM会在字符串常量池中存储这个新创建的字符串对象的引用
- 如果字符串常量池已经有字符串对象“abc”的引用
- JVM会在堆内存中创建一个新的字符串对象实例(“abc”)
- JVM 将变量直接指向字符串常量池中已经存在的那个字符串对象的引用
7. String中的intern方法有什么作用?
String.intern()
是一个native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接放回该引用
- 如果字符串常量池中没有保存对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回
// 在堆中创建字符串对象”Java“
// 将字符串对象”Java“的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); //true
8. 重载和重写的区别?⭐️⭐️⭐️⭐️⭐️
答:方法的重载和重写都是实现多态的方式。
区别在于以下几点:
发生阶段:
- 重载是编译时多态
- 重写是运行时多态
发生范围
参数列表、返回类型、异常、访问修饰符
- 重载:最常用的场景——构造函数,在一个类中同名方法有不同的参数列表
- 重写:最常用的场景——继承,子列重写父类的方法,参数列表必须与父类完全相同
- 其他方面,重写的要求都更为严格:
- 子类重写的返回类型必须兼容父类,也就是说返回类型要相同或者是子类型
- 异常不能抛出更宽泛的异常
- 访问修饰符:不能降低访问级别
9. 内部类了解吗?匿名内部类了解吗?⭐️⭐️⭐️
- 内部类分为下面4种:
- 成员内部类
- 静态内部类
- 方法内部类
- 匿名内部类
//我的理解
什么是内部类?
- 内部类就是写在类里面的类,而匿名内部类就是没命名的写在类里面的类
都有啥内部类?
- 类里面有各种结构,直接在类里面嵌套一个类的就是
成员内部类
;在成员内部类
前加一个static
就是静态内部类;定义在类的方法里的类就是方法内部类
;没命名的就是匿名内部类
、为啥要用内部类?
五. 反射
1. Java反射?反射有什么优点/缺点?你是怎么理解反射的(为什么框架需要反射)?⭐️⭐️⭐️⭐️⭐️
(1)何为反射?
- 反射是框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力
- 通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性
(2)谈谈反射机制的优缺点?
优点
- 可以让代码更灵活
- 为各种框架提供 开箱即用的功能提供了便利
缺点
- 安全问题:让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查
- 反射的性能也要稍差点,不过,对于框架来说实际影响不大
(3)你是怎么理解反射的(为什么框架需要反射)?
动态性
- Java是非动态的语言——简单来说就是要先编译再运行,不可以在运行时对代码进行修改(C和C++也是非动态的),它们的优点就是性能好,更易于维护,且更严谨,更易于维护。
- 但是动态语言(如Python,JavaScript等)能够在代码执行中对程序变量进行修改,开发更高效、快速,有更好的交互性
- 反射机制让Java可以具备动态语言的优点,使得程序可以在运行时动态地加载、探查和使用Java类。它允许框架在不牺牲灵活性的前提下,支持各种不同的应用场景。
和注解的搭配使用–》绝绝子!!
- 注解本身仅仅是起到标记作用,它需要利用反射机制,根据注解标记去调用注解解释器,执行行为。如果没有反射机制,注解并不比注释更有用。
2. 获取Class对象的四种方式
- 通过
对象实例.getClass()
获取
Person p = new Person();
Class c1 = p.getClass();
- 通过
Class.forName()
传入类的全路径获取
Class c2 = Class.forName("com.example.pojo.Person");
- 通过
类名.class
获取
Class c3 = Person.class;
- 通过类加载器
xxxClassLoader.loadClass()
传入类路径获取
ClassLoader.getSystemClassLoader().loadClass("com.example.pojo.Person");
3. 反射的应用场景?
注解,动态代理
当然最多的框架
六. 注解
1. 谈谈Java的注解?解决了什么问题?
Java注解
注解是Java5引入的新特性。主要用于修饰类、方法或变量,提供某些信息供程序在编译或运行时使用。注解的本质就是继承了
Annotation
注解
解决了什么问题?
- 其实注解和注释某种程度上,效果差不多
- 比如@Override注解,如果程序员写错了,那就会在编译时报错,提示要修改
- 比如@Deprecated注解用于标记已弃用的代码,开发人员在使用过程中会得到相应的警告
- 注解结合反射机制
大大提高了框架的灵活性
2. 注解的解析方法有哪几种?
注解只有被解析之后才会生效,常见的解析方法有两种:
- 编译器直接扫描:比如
@Override
注解,编译器在编译时就会监测当前方法是否重写了父类对应的方法- 运行期通过反射处理:像框架中自带的注解(比如Spring框架的@Value , @Componet)都是通过反射来进行处理的。
七. 泛型
(1)什么是泛型?有什么作用?
- Java泛型是JDK5中引入的一个新特性 . 使用泛型参数 , 可以增强代码的可读性和稳定性
- 编译器可以对泛型参数进行监测 , 并且通过泛型参数可以指定传入的对象类型 . 比如
ArrayList<Person> persons = new ArrayList<person>()
这行代码就指明了该ArrayList
对象只能传入Person
对象 , 如果传入其他类型的对象就会报错.
(2) 什么是类型擦除(泛型擦除机制)? 为什么要擦除?⭐️⭐️⭐️⭐️
- Java的泛型是伪泛型 , 这是因为Java在编译期间 , 所有的泛型信息都会被擦掉 , 这也就是通常所说的类型擦除
- 为什么要擦除 ? 因为泛型是在java1.5之后引入的 , 为了能够兼容之前版本的代码 , 才要进行泛型擦除
(3) 泛型有哪些限制?为什么?
泛型的限制一般是由泛型擦除机制导致的 . 擦除为
Object
后无法进行类型判断
比如 :
- 只能声明不能实例化
T
类型变量- 不能实例化泛型数组
- 不能实例化泛型参数的数组 , 擦除后为
Object
后无法进行类型判断- 不能实现两个不同泛型参数的同一接口 , 擦除后多个父类的桥方法将冲突
- 不能使用static修饰泛型变量
(4) 为什么要用泛型 , 用Object
不行吗?
其实在Java中所有类型都是Object的子类型 , 用Object去定义当然没有问题 , 毕竟泛型在编译时也会进行泛型擦除 , 其中一部分泛型也会被转为Object类型
用Object , 虽然写代码的时候不会报错 , 但你需要时刻注意你的逻辑 比如:Cat cat = new Cat(); Dog dog = new Dog(); //泛型 ArrayList<Cat> list = new ArrayList(); list.add(cat); list.add(dog); //报错 //Object ArrayList<Object> list = new ArrayList(); list.add(cat); list.add(dog); //不会报错 ⇒ 但编译时会报错
八. IO流
其他(打字打累了😭😡 🤬 这里开始截屏+思路)
1. 什么是字节码?字节码的好处是什么?
什么是字节码
- java中,JVM能看懂的代码就是字节码(就是.class文件)
- 它不面向任何处理器,只面向虚拟机
好处
- java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。
- 字节码并不是针对一种特定的机器,因此,Java程序无需重新编译便可在多种不同操作系统的计算机上运行
值得注意的是
- 普通的编译运行的步骤可能是JVM类加载器首先加载字节码文件,然后通过解释器逐行进行解释,这种方式的执行速度回相对比较慢。
- 而且有些方法和代码块是经常被需要被调用的—》引进了JIT(Just in Time Compilation)编译器,而JIT属于运行时编译(即时编译器)
- JIT编译器完成第一次编译后,会将字节码对应的机器码保存下来,下次可以直接使用。–》也因此称“Java是编译与解释共存的语言”
- 只有热点代码(HotSpot)才会被JIT编译–》
- 怎么确定是HotSpot?–》JVM中有一个阈值,当方法/代码块在一定时间内被调用的次数超过这个阈值,就会存入codeCache中(codeCache中存的是解释器解释之后的机器码)。当下次执行时,再遇到这段代码,就会从codeCache中读取机器码,直接执行
2. JDK,JRE,JVM,JIT的关系
3. 为什么说Java是编译与解释并存?
前情
为什么?
java代码的执行步骤是:
- 编译:代码通过javac 编译称 字节码文件(.class)
- 解释:字节码文件 通过 解释器 转换为 机器语言
AOT有什么优点?为什么不全部使用AOT?
- AOT这么给力,为什么不全部使用AOT?
因为AOT不支持Java的一些动态特性,如动态代理、反射等
而很多框架都用到了这些特性,
而JIT支持,所以选用JIT
额,暂时没整理全,但是先放这一部分
1. 序列化和反序列化
- 序列化:将数据结构或对象 转换成 二进制字节流的过程
- 反序列化:将二进制字节流 转化成 数据结构或对象的过程