【Java基础】八股总结一:String、包装类、异常、反射

156 阅读13分钟

1. Java 中的几种基本数据类型是什么?对应的包装类型是什么?各自占用多少字节呢?

字节包装类(包装类缓存)
byte1Byte[-128, 127]
short2Short[-128, 127]
int4Integer[-128, 127]
long8Long[-128, 127]
char2Character[0, 127]
float4Float 无缓存
double8Double 无缓存
boolean1bit(不是1字节)Boolean (True/False均缓存)

为什么要有包装类?

  1. java是面向对象的,很多常见需要对象
  2. 满足泛型的要求,泛型类型参数只能是对象类型
  3. 包装类允许null值

2. String 、 StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?

StringBuffer和StringBuilder都继承AbstractStringBuilder,( 可变性) 其中字符数组没有加final关键字,然后从线程安全性考虑,然后从性能比较,最后说适用的场景


可变性:String 是不可变的。StringBuilder 与 StringBuffer可变,StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。

线程安全性: String不可变的,线程安全。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的StringBuilder ****并没有对方法进行加同步锁,所以是非线程安全的

性能: 相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

  1. 操作少量的数据: 适用 String
  2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

String的为什么不可变?

  1. 首先final修饰的变量,如果是引用类型(char[]),则不能指向其他对象。
  2. 其次char[] 被private修饰 且 String类没有提供修改这个字符数组本身的方法(理论上可以修改数组内容,只是不能将value指向其他数组),那么外部无法修改字符数组的内容。
  3. String类被final关键字修饰不能继承,避免子类破坏 不可变性。

Java9以后String的底层实现 由char[] 变为 byte[]


如何破坏String的不可变?

通过反射,破坏String类的不可变。

反射可以获取到String类对象中的私有成员。

final关键字的特性?

  1. 修饰的类不能被继承。
  2. 修饰的普通变量值不能被修改。
  3. 修饰的引用变量 引用不能指向其他对象。
  4. 修饰的方法不能被重写。

字符串拼接用“+” 还是 StringBuilder?

字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象

不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象

3. String s1 = new String("abc");这段代码创建了几个字符串对象?

看情况,可能创建一个也可能创建两个

  • 如果字符串常量池StringTable中存在"abc"的引用,则会在堆中创建一个对象
  • 如果字符串常量池StringTable中不存在"abc" 对象的引用,则会在堆中创建两个,其中一个对象的引用保存到常量池中,另一个对象引用赋给s1。

4. == 与 equals?hashCode 与 equals ?

  • ==
    • 基本数据类型比较 值
    • 引用数据类型 比较 对象内存地址
  • equals(是Object类的方法,默认为==
    • 如果类没有重写,与==相同,比较内存地址。
    • 如果类重写了,按重写equals的规则,一般是对象中的某些属性相等则相等。
  • hashCode(是Object类的native方法,计算对象的hashCode值)
    • 用于HashSetHashMap计算对象hash值
    • 两对象hashCode相等,对象不一定相等(可能碰撞) ,hashCode不相同则对象一定不相同。

包装类

5. 包装类型的缓存机制了解么?

  • Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127]相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False
  • 超过缓存范围将 new新的对象
  • 所有整型包装类对象之间值的比较,全部使用 equals 方法比较,用==比较是一个大坑(超出缓存范围将在堆上比较对象地址)。

源码

6. 自动装箱与拆箱了解吗?原理是什么?

  • 装箱:将基本类型 转为 包装类型,原理,例:Integer i = Integer .valueOf(10)
  • 拆箱:将包装类 转换为 基本类型,原理,例:int n = i.intValue()

7. 深拷贝和浅拷贝区别了解吗?什么是引用拷贝?

文章详解:【Java】深拷贝浅拷贝(Java实现)_java反射实现浅拷贝-CSDN博客

浅拷贝: 继承cloneable接口,重写clone方法并调用super.clone()。对象中值类型会拷贝一份,对象中的引用类型变量仅复制地址,而不会创建新对象。

深拷贝: 继承cloneable接口,重写clone方法并调用super.clone(),还需要调用成员引用对象的clone()方法(前提是引用类型以及重写了)。对象中值类型会拷贝一份,对象中的引用类型对象也会拷贝一份创建新对象。

cloneable接口也是一个标记接口,clone方法是Object类的方法

@Override
public Person clone() {
    try {
        Person person = (Person) super.clone();
        person.setAddress(person.getAddress().clone());
        return person;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

引用拷贝:简单的理解为对象赋值。

序列化 反序列化实现深拷贝

虽然利用调用成员引用类型的clone方法可以完成深拷贝,但在对象引用多层套娃时会比较繁琐(每个嵌套的引用都要调用clone),可以使用序列化反序列化解决多层套娃时深拷贝问题

    @Override
    protected Student clone() {
        try {
            // 将对象本身序列化到字节流
            // 利用 字节数组输出流-->对象输出流 写到内存
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            ObjectOutputStream objectOutputStream =
                    new ObjectOutputStream( byteArrayOutputStream );
            objectOutputStream.writeObject( this );

            // 再将字节流通过反序列化方式得到对象副本
            // 利用 字节数组输入流-->对象输出流 从字节数组中 读取对象
            ObjectInputStream objectInputStream =
                    new ObjectInputStream( new ByteArrayInputStream( byteArrayOutputStream.toByteArray() ) );
            return (Student) objectInputStream.readObject();

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

深拷贝浅拷贝的应用场景?

在某一个场景(不可变类的get方法)中可以用到

比如我们设计了一个不可变类,对于不可变类中的 引用成员。我们不提供set方法让外部修改成员,但提供了get方法让外部获取内部引用成员。

这个get方法提供给外部的引用成员,外部拿到后其实是可以修改引用成员的内容的,如何使得让get方法让外部获取成员,但又不让外部改变我们的引用成员呢?

这时可以利用深拷贝,get方法返回给外部的是一个引用成员的克隆,这样外部获取到内部成员的克隆后就无法破坏不可变类

序列化、反序列化

从三个方面来说。序列化方式方法,接口作用,序列化ID的作用

序列化/反序列化,我忍你很久了,淦!

序列化方式方法

以字节的方式,序列化为文件(FileInput/OutputStream)字节数组(ByteArrayInput/OutStream)

    ObjectOutputStream objectOutputStream = 
        new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
    objectOutputStream.writeObject( student );
    ObjectInputStream objectInputStream = 
        new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
    Student student = (Student) objectInputStream.readObject();
    objectInputStream.close();

Serializable接口的作用

标记接口

如果一个对象既不是字符串数组枚举,而且也没有实现Serializable接口的话,在序列化时就会抛出NotSerializableException异常!

serialVersionUID序列化id

  1. serialVersionUID序列化ID,可以看成是序列化和反序列化过程中的“暗号”,在反序列化时,JVM会把字节流中的序列号ID和被序列化类中的序列号ID做比对,只有两者一致,才能重新反序列化,否则就会报异常来终止反序列化的过程
  2. 如果在定义一个可序列化的类时,用户(程序员)没有显式地给它定义一个serialVersionUID的话,则Java运行时环境会根据该类的各方面信息自动地为它生成一个默认的serialVersionUID一旦像上面一样更改了类的结构或者信息,则类的serialVersionUID也会跟着变化
  3. 如果用户/程序员声明了serialVersionUID即使在序列化完成之后修改了类导致类重新编译,则原来的数据也能正常反序列化,只是新增的字段值是默认值而已

8. 谈谈对 Java 注解的理解,解决了什么问题?

听说你只会用注解,不会自己写注解?

注解运行时 配合反射使用,为spring框架提供了便利。

异常

Exception 和 Error 有什么区别?

  1. 首先都是Throwable的子类, Throwable是一个类。
  2. Exception 异常 :程序本身可以处理的异常,可以通过 catch 来进行捕获。异常又分为受检查异常非受检查异常
  3. Error:程序无法处理的错误,不建议通过catch捕获。如OOMError(堆内存溢出),Virtual MachineError(虚拟机错误),StackOverFlowError(栈溢出)。

Checked Exception 和 Unchecked Exception 有什么区别?

  1. Checked Exception受检查异常
    1. 也叫编译时异常,在代码中必须 catch 或者 throws,不然过不了编译。
    2. 除了RuntimeException及其子类以外其他都是受检查异常。
    3. ClassNotFoundException(类找不到),IOException(IO相关) ,SQLException(sql异常)
  1. Unchecked Exception非受检查异常
    1. 也叫运行时异常,编译时不处理这种异常也能过编译
    2. 所有 RuntimeException 及其子类
    3. NullPointerException空指针异常、ArrayIndexOutOfBoundsException数组越界异常、ArithmeticException算术异常(除0)

反射

10. Java 反射?反射有什么缺点?你是怎么理解反射的(为什么框架需要反射)?

反射:

  • 反射之所以被称为框架的灵魂,!!!!!!!反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,都能够调用它的任意一个方法。它赋予了我们在运行时分析类以及执行类中方法的能力
  • 主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
  • 应用场景:
    • 正是因为反射,你才能这么轻松地使用各种框架SpringIOC就是通过反射创建bean的,像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
    • 动态代理AOP的实现也依赖于反射
    • 注解 的使用也依赖于反射(例如框架里Component和value等注解)。

反射优缺点:

  • 优点:代码更加灵活,运行动态获取类和使用类。为各种框架提供了开箱即用的功能。
  • 缺点:
    • 运行时操作分析类,增加了安全问题(因为可以通过反射获得 私有方法和属性)
    • 额外的性能开销,对于框架来说影像不大。

11. Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符?

x、泛型

美团一面:为什么要使用泛型?使用泛型带给你的好处有哪些 ?差点翻车。。。_哔哩哔哩_bilibili

泛型有泛型类/泛型接口 和 泛型方法

  • 泛型:
  • 类型擦除:
  • 通配符:

为什么要用泛型?

看Java核心技术后 的 总结

  1. 泛型其实是一种 类的参数。使用泛型意味着 代码可以被不同的类型的对象重用提高代码复用性
  2. 如果不使用泛型,例如在使用 集合类 时需要为每个类型都创建一个集合类。(使用Object不行吗?Object强转 可读性低 且 不安全可能出现转换错误
  3. 如果使用泛型,例如在集合类 中使用泛型,可以用一个集合类聚集任何类的对象。

内部类

12. 内部类了解吗?匿名内部类了解吗?

x、内部类

内部类主要有 成员内部类、局部内部类、匿名内部类、静态内部类。

成员内部类:成员内部类定义在外部类的成员位置,可以访问外部类的所有成员变量和方法。

局部内部类:局部内部类定义在方法或作用域内部,只能在所在的方法或作用域中访问。

匿名内部类:匿名内部类没有名字,匿名内部类常用于实现接口或继承普通类 直接在创建对象的地方进行定义和实例化。

静态内部类: 静态内部类是在一个类内部定义的静态类它不依赖于外部类的实例,可以直接使用外部类的静态成员,而不需要创建外部类对象

匿名内部类

匿名内部类常用于实现接口或继承普通类

实现接口的 匿名内部类

Contents是接口

    //匿名内部类实现接口方式2
    Contents obj2 = new Contents(){
        private int value = 2;
        public int getValue(){return value;}
    };

继承 普通类的 匿名内部类

BaseClass 是一个普通的类

// 使用匿名内部类扩展普通类
BaseClass baseObject = new BaseClass() {
    @Override
    public void doSomething() {
        System.out.println("Anonymous inner class is doing something.");
    }
};

静态内部类 和 非静态内部类 区别

记前两点就行,访问方式和创建方式

创建方式

  • 静态内部类可以不依赖于外部类实例被创建。它可以直接通过外部类.内部类的方式被创建。
  • 非静态内部类需要依赖于外部类实例被创建,它必须先有一个外部类实例才能创建内部类实例。
  • 非静态内部类依赖于外部类的实例,而静态内部类不依赖于外部类的实例非静态内部类在外部类实例化后才能实例化,而静态内部类可以独立实例化。

访问方式:

  • 静态内部类可以访问外部类的静态成员,如果需要访问外部类的非静态成员,则需要外部类的实例。
  • 非静态内部类可以访问外部类的私有变量和方法,无论是静态还是非静态。
  • 非静态内部类可以访问外部类的私有实例变量和方法,而静态内部类只能访问外部类的静态成员

非静态内部类不能定义静态成员,而静态内部类可以定义静态成员。

public class test {
    public static void main(String[] args) {
        OuterClass out = new OuterClass("hhb", 20);
		//创建内部类对象方式1
        //使用 .new 语法,通过外部类对象直接创建内部类对象
        OuterClass.innerClass innerObj2 = out.new innerClass();
        innerObj2.innerMethod();
    }
}

拓展:静态内部类只有在使用的时候才会加载和初始化。


public class OuterClass {
    public static class InnerClass{
        public static void print(){
            System.out.println("调用静态内部类方法");
        }
        // 类加载执行
        // 八股:类加载的三大步骤:加载、链接、初始化
        // static代码块在初始化步骤执行
        static {
            System.out.println("静态内部类加载了");
        }
    }

    public static void main(String[] args) throws Exception{
        OuterClass outerClass = new OuterClass();
        System.out.println("外部类加载了");
        Thread.sleep(1000);
        System.out.println("休眠结束");
        // 这个时候才加载静态内部类,使用的时候才加载
        OuterClass.InnerClass.print();
        // 静态内部类实例的创建方式
        OuterClass.InnerClass innerClass = new OuterClass.InnerClass();
    }
}

13. BIO,NIO,AIO 有什么区别?(重要)

BIO、NIO、AIO