Java基础(八股)

Java基础


!!!!本文是基于javaGuide开源项目进行学习的记录和自我总结!!!!

Java基础

一. 数据类型

1. Java有哪些基础数据类型? 它们的默认值和占用空间大小知道不? 说说这些基础数据类型对应的包装类型?⭐️⭐️⭐️⭐️

  • 8中基本数据类型:
    byte , short , int , long , float , double , char , boolean

  • 默认值和占用空间大小

基本类型位数bit字节Byte默认值取值范围
byte810-127 ~ 128
short1620-2^15 ~ 2^15-1
int3240-2^32 ~ 2^32-1
long6480L-2^63 ~ 2^63-1
float3240f
double6480d
char162‘uoooo’
boolean1falsetrue , false
  • 对应的包装类型
基本类型包装类型
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

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)供外部来操作属性

  • 继承
  • 不同类型的对象,相互之间经常有一定数量的共同点。
  • 继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。

//注意

  1. 子类拥有父类的所有属性和方法,是父类中的私有属性和方法子类无法访问只是拥有
    2.子类可对父类进行扩展,即可以定义自己的属性和方法
  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"
    }
}

//详述

  • 多态
    多态就是可以让一个对象在不同条件下表现为不同的形式,具体表现为 父类的引用指向子类的对象
  • 多态的前提(对上面代码进行分析)
  1. 继承与实现:在多态中必须存在有继承或实现关系的子类和父类
`class Dog extends Animal` //Dog类继承了Animal类
/*
必须要在子类继承父类的条件下,
是因为 继承本质上是为了扩展父类的功能,
而多态要在这样的前提下,让父类也能够使用子类扩展的这些东西
*/
  1. 方法的重写:子类重写了父类的某些方法(@Override)
//重写了父类的makeSound方法
@Override
   void makeSound() {
       System.out.println("Bark");
   }
   /*
		因为子类就是为了扩展父类而存在的,
		所以它可能会重写父类的一些方法或自己定义一些新方法
		--》那么多态情况下,父类引用调用的是谁的方法和属性呢?下面有解释
   */
  1. 父类引用指向子类对象
 //父类引用:Aniaml myPet;子类对象(实例)new Dog();
   Animal myPet = new Dog(); 
/*
这样做 就是为了 让父类能够调用子类扩展的东西
*/

  • 多态的特点
  1. 多态时,访问父类的成员变量
  2. 多态时,子父类存在同名的非静态成员方法时,访问的是子类中重写的方法
  3. 多态时,子父类存在同名的静态成员变量、成员方法时,访问的父类的成员函数
  4. 多态时,不能访问子类独有的方法

8. static和final关键字

上面很多地方都提到了static和final关键字,这里我们来简单了解一下

  • static
  • static修饰的成员变量和方法,是属于类的–》可以通过类名直接访问(我们见过的Math.min() ,这个就是static的使用场景之一)
  • 为什么要用static?什么时候用static?(举几个简单的例子)
  1. 共享状态。当多个对象需要共享相同的数据时,可以使用static变量来存储这些共享状态
  2. 工具类方法。工具类(如Math、Collections等),我们常见的Math.max() 就是通过类名直接调用
  3. 单例模式等
  • final
  • final关键字很神奇
  • final修饰变量,有两种可能:基本数据类型和引用类型。如果修饰的是基本数据类型,那基本数据类型不可变,但是如果修饰的是引用数据类型对象引用不可变,但是!!!对象实例的内容是可以改变的
  • final修饰变量,变量不可改变,且成员变量必须在定义时初始化(三种方法),局部变量必须在使用前初始化,且只能被初始化一次
1.为什么 要初始化?
因为final修饰的变量,一旦被初始化就不能被重新赋值,所以一定要在使用之前完成对他们的创建(也就是给他们赋值)

2. 为什么成员变量和局部变量初始化时机不一样?
- 上面说“因为final修饰的变量,一旦被初始化就不能被重新赋值,所以要在使用之前完成初始化”
- 而成员变量和局部变量初始化的时机也和他们被使用的时机有关
- 成员变量:成员变量与实例对象关联,它们在·对象创建时·就应该有确定的值
- 局部变量:局部变量与方法调用有关,他们作用域小,所以不着急初始化
成员变量初始化的三种方法
- 声明时初始化:final int value = 10;
- 构造器中初始化:public MyClass(){this.value = 10;}
- 实例初始化块中初始化:{ value = 10;}
  • final修饰方法,方法不能被重写,但可以被继承
  • final修饰类,类不能被继承

9. 接口和抽象类有什么共同点和区别?⭐️⭐️⭐️⭐️⭐️

  • 共同点
  1. 都不能被实例化
  2. 都可以包含抽象方法,且抽象方法不能有方法体,必须在子类或实现类中实现
  • 区别(接口比抽象类更抽象–》更纯粹)

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,那发生什么了呢?

  1. 将“afajie”存储在字符串常量池中:如果常量池中已经存在一个相同的字符串“afajie”,那么会重用这个对象
  2. 引用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数组被定义为privatefinal
  • 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修饰–》所以是可变的
  • 还可以从StringBufferStringBuilder的源码中看到–》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个字符串对象
  1. 如果字符串常量池中没有字符串对象“abc”的引用
  • JVM会在堆内存中创建一个新的字符串对象实例(“abc”)
  • 然后,JVM会在字符串常量池中存储这个新创建的字符串对象的引用
  1. 如果字符串常量池已经有字符串对象“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对象的四种方式

  1. 通过对象实例.getClass()获取
Person p = new Person();
Class c1 = p.getClass();
  1. 通过Class.forName()传入类的全路径获取
Class c2 = Class.forName("com.example.pojo.Person");
  1. 通过类名.class获取
Class c3 = Person.class;
  1. 通过类加载器xxxClassLoader.loadClass()传入类路径获取
ClassLoader.getSystemClassLoader().loadClass("com.example.pojo.Person");

3. 反射的应用场景?

注解,动态代理
当然最多的框架

六. 注解

1. 谈谈Java的注解?解决了什么问题?

Java注解

注解是Java5引入的新特性。主要用于修饰类、方法或变量,提供某些信息供程序在编译或运行时使用。注解的本质就是继承了Annotation注解

解决了什么问题?

  1. 其实注解和注释某种程度上,效果差不多
  • 比如@Override注解,如果程序员写错了,那就会在编译时报错,提示要修改
  • 比如@Deprecated注解用于标记已弃用的代码,开发人员在使用过程中会得到相应的警告
  1. 注解结合反射机制

大大提高了框架的灵活性

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代码的执行步骤是:

  1. 编译:代码通过javac 编译称 字节码文件(.class)
  2. 解释:字节码文件 通过 解释器 转换为 机器语言

AOT有什么优点?为什么不全部使用AOT?

在这里插入图片描述

  • AOT这么给力,为什么不全部使用AOT?

因为AOT不支持Java的一些动态特性,如动态代理、反射等
而很多框架都用到了这些特性,
而JIT支持,所以选用JIT

额,暂时没整理全,但是先放这一部分

1. 序列化和反序列化

  • 序列化:将数据结构或对象 转换成 二进制字节流的过程
  • 反序列化:将二进制字节流 转化成 数据结构或对象的过程
Java基础八股文面试题 1. Java的特点是什么? Java语言具有面向对象、跨平台、安全性、可靠性、可移植性、多线程等特点。 2. Java的基本数据类型有哪些? Java的基本数据类型包括整型、浮点型、字符型、布尔型。 3. Java中的变量命名规则是什么? Java中的变量命名规则是遵循驼峰命名法,即第一个单词小写,后面的每个单词首字母大写。 4. Java中的四种访问权限分别是什么? Java中的四种访问权限分别是public、private、protected、default。 5. Java中的final关键字有什么作用? Java中的final关键字用来修饰一个变量、一个方法或一个类,分别表示不可变、不可重写和不可继承。 6. Java中的抽象类和接口有什么区别? Java中的抽象类和接口都是用来实现多态性的机制,但是抽象类可以包含非抽象方法和属性,而接口只能包含抽象方法和常量。另外,一个类只能继承一个抽象类,但是可以实现多个接口。 7. Java中的异常处理机制是什么? Java中的异常处理机制是通过try-catch-finally代码块实现的,当程序发生异常时,会抛出一个异常对象,可以通过try-catch语句捕获并处理异常。 8. Java中的线程有哪些状态? Java中的线程有五种状态,分别是新建状态、就绪状态、运行状态、阻塞状态和死亡状态。 以上是Java基础八股文面试题的答案,希望能够帮助到您。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值