Skip to content

Java基础

更新: 2/8/2025 字数: 0 字 时长: 0 分钟

Java中的序列化和反序列化是什么?

序列化是将对象转换为字节流的过程,这样对象可以通过网络传输、持久化存储或者缓存。Java 提供了java.io.serializable接口来支持序列化,只要类实现了这个接口,就可以将该类的对象进行序列化。

反序列化是将字节流重新转换为对象的过程,即从存储中读取数据并重新创建对象。

注意

  1. 应用场景:包括网络传输、远程调用、持久化存储(如保存到文件或数据库)、以及分布式系统中数据交换。 Java 序列化关键类和接口:objectoutputStream 用于序列化,objectInputStream 用于反序列化。类必须实现 serializable 接口才能被序列化。
  2. transient 关键字:在序列化过程中,有些字段不需要被序列化,例如敏感数据,可以使用transient 关键字标记不需要序列化的字段。
  3. serialVersionUID:每个 serializable 类都应该定义一个serialversionuID,用于在反序列化时验证版本一致性。如果没有明确指定,Java 会根据类的定义自动生成一个UID,版本不匹配可能导致反序列化失败。
  4. 序列化性能问题:Java 的默认序列化机制可能比较慢,尤其是对于大规模分布式系统,可能会选择更加高效的序列化框架(如 Protobuf、Kryo)。
  5. 安全性:反序列化是一个潜在的安全风险,因为通过恶意构造的字节流,可能会加载不安全的类或执行不期望的代码。因此,反序列化过程需要进行输入验证,避免反序列化漏洞。
  6. 序列化无法存储静态变量是因为,静态变量属于类级别的,与类的定义相关联。更具体的说序列化是通过调用对象的writeObject方法和readObject来实将对象写入输出流和读取输入流的,而静态变量由于不属于对象的一部分,因此调用这两个方法时候静态变量都不参与其中,也行成一开始说的序列化无法存储静态变量的值。

代码

java
import java.io.*;

// 定义一个可序列化的类
class Employee implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

public class SerializationExample {
    public static void main(String[] args) {
        // 创建一个 Employee 对象
        Employee employee = new Employee("John Doe", 30);

        // 序列化对象
        try (FileOutputStream fileOut = new FileOutputStream("employee.ser");
             ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
            out.writeObject(employee);
            System.out.println("对象已序列化并保存到 employee.ser 文件中");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 反序列化对象
        Employee deserializedEmployee = null;
        try (FileInputStream fileIn = new FileInputStream("employee.ser");
             ObjectInputStream in = new ObjectInputStream(fileIn)) {
            deserializedEmployee = (Employee) in.readObject();
            System.out.println("对象已从 employee.ser 文件中反序列化");
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

        // 验证反序列化后的对象
        if (deserializedEmployee != null) {
            System.out.println("姓名: " + deserializedEmployee.getName());
            System.out.println("年龄: " + deserializedEmployee.getAge());
        }
    }
}

什么是Java中的不可变类?

不可变类是指在创建后其状态(对象的字段)无法被修改的类。一旦对象被创建,它的所有属性都不能被更改。这种类的实例在整个生命周期内保持不变。

关键特征:

  1. 声明类为final,防止子类继承。
  2. 类的所有字段都是 private 和final,确保它们在初始化后不能被更改。
  3. 通过构造函数初始化所有字段。
  4. 不提供任何修改对象状态的方法(如setter方法)。
  5. 如果类包含可变对象的引用,确保这些引l用在对象外部无法被修改。例如getter方法中返回对象的副本(new一个新的对象)来保护可变对象。
  6. Java 中的经典不可变类有:String、Integer、BigDecimal、LocalDate 等。

优点

  1. 线程安全:由于不可变对象的状态不能被修改,它们天生是线程安全的,在并发环境中无需同步。

  2. 缓存友好:不可变对象可以安全地被缓存和共享,如string的字符串常量池。

  3. 防止状态不一致:不可变类可以有效避免因意外修改对象状态而导致的不一致问题。

缺点

  1. 性能问题:不可变对象需要在每次状态变化时创建新的对象,这可能会导致性能开销,尤其是对于大规模对象或频繁修改的场景(例如 String 频繁拼接)。

Java 中 Exception 和 Error 有什么区别?

ExceptionError都是Throwable类的子类(在Java代码中只有继承了Throwable类的实例才可以被 throw或者被catch)它们表示在程序运行时发生的异常或错误情况。

Exception:是程序中可以处理的异常情况,表示程序逻辑或外部环境中的问题,可以通过代码进行恢复或处理。

Error:表示严重的错误,通常是JVM 层次内系统级的、无法预料的错误,程序无法通过代码进行处理或恢复。

注意

  1. 尽量不要捕获Exception,捕获特定的异常,除了别人可以看懂之外,也可以避免捕获不想捕获的异常;
  2. 捕获异常之后,需要明确的将异常信息记录到日志中;
  3. 在可能出现异常的地方尽早捕获,不要在调用了好多个方法之后捕获;
  4. 只在有必要try catch的地方 try catch;
  5. 可以使用if/else来判断的就不要用异常,因为异常肯定比条件语句低效;
  6. 不要在finally中return或者处理返回值。

image-20250204222557440

你认为Java的优势是什么?

跨平台、垃圾回收、生态、面向对象

  1. 跨平台(可移植性,通过jvm实现)-一次编写,处处运行
  2. 垃圾回收-自动回收内存,提高内存管理效率
  3. 相关生态-强大的类库和第三方组件
  4. 面向对象-封装、继承、多态

什么是Java 的多态特性?

多态是指同一个接口或父类引用变量可以指向不同的对象实例,并根据实际指向的对象类型执行相应的方法。

它允许同一方法在不同对象上表现出不同的行为,是面向对象编程(OOP)的核心特性之一。

多态的优点

通过多态,程序可以灵活地处理不同类型的对象,降低代码耦合度,增强系统的可扩展性。新增子类或实现类时,无需修改原有代码,只需通过接口或父类引用调用即可。

java
// 定义一个父类 Animal
class Animal {
    // 定义一个方法 makeSound
    public void makeSound() {
        System.out.println("动物发出声音");
    }
}

// 定义 Dog 类,继承自 Animal 类
class Dog extends Animal {
    // 重写父类的 makeSound 方法
    @Override
    public void makeSound() {
        System.out.println("汪汪汪");
    }
}

// 定义 Cat 类,继承自 Animal 类
class Cat extends Animal {
    // 重写父类的 makeSound 方法
    @Override
    public void makeSound() {
        System.out.println("喵喵喵");
    }
}

public class PolymorphismExample {
    public static void main(String[] args) {
        // 创建 Animal 类型的引用,指向 Dog 对象
        Animal dog = new Dog();
        // 创建 Animal 类型的引用,指向 Cat 对象
        Animal cat = new Cat();
        
        // 直接创建 Animal 对象并调用 makeSound 方法
        Animal animal = new Animal();
      

        // 调用 makeSound 方法
        dog.makeSound(); 
        cat.makeSound(); 
        
        animal.makeSound(); 
        
    }
}

//汪汪汪
//喵喵喵
//动物发出声音

Java中的参数传递是按值还是按引用?

在Java中,参数传递只有按值传递,不论是基本类型还是引用类型。

基本数据类型(如int,char,boolean 等):传递的是值的副本,即基本类型的数值本身。因此,对方法参数的任何修改都不会影响原始变量。

引用数据类型(如对象引用):传递的是引用的副本,即对象引用的内存地址。因此,方法内可以通过引用修改对象的属性,但不能改变引用本身,使其指向另一个对象。

为什么Java不支持多重继承?

因为多继承会产生菱形继承(也叫钻石继承)问题

为什么接口可以多实现?

在Java8之前接口是无法定义具体方法实现的,所以即使有多个接口必须子类自己实现,所以并不会发生歧义。

Java8之后出了默认方法(default method),此时就又出现的多继承的菱形继承问题了

所以Java强制规定,如果多个接口内有相同的默认方法,子类必须重写这个方法。不然,编译期就会报错

继承决定了类的“本质”,接口扩展了类的“能力”。 好比:

一个生物的“本质”取决于ta的父母,无法改变,这就是“继承”关系。

一个生物的“能力”一部分是从ta的父母那里继承过来的,但是更多“能力”是通过后天学习而来的,这就是自我“实现”的"能力"。

Java面向对象编程与面向过程编程的区别是什么?

面向对象编程(ObjectOrientedProgramming,OOP)是一种对象为中心的编程范式或者说编程风格。把类或对象作为基本单元来组织代码,并且运用提炼出的:封装、继承和多态来作为代码设计指导。

面向过程编程是一种以过程或函数为中心的编程范式或者说编程风格,以过程作为基本单元来组织代码。过程其实就是动作,对应到代码中来就是函数,面向过程中函数和数据是分离的,数据就是成员变量。

总结来看:面向对象编程注重对象之间的交互和模块化设计,而面向过程编程注重逻辑的分步实现

区别

  1. 思维方式: 面向对象:通过定义对象的属性和行为来解决问题,关注对象之间的关系和交互。

    面向过程:通过函数或过程一步步实现业务逻辑,关注执行的步骤和顺序。

  2. 数据与行为的关系: 面向对象:数据和行为封装在对象内部,数据操作由对象方法进行管理。

    面向过程:数据和函数是分离的,函数对数据进行操作。

  3. 可扩展性和复用性: 面向对象:通过继承、接口、多态等机制支持代码的高复用性和扩展性。

    面向过程:复用性较低,扩展需要修改已有代码,影响整体稳定性。

  4. 适用场景: 面向对象:适合处理复杂的系统和模块化设计,便于维护和扩展。

    面向过程:适用于一些简单、顺序性强的小型程序,开发效率较高。

面向对象的三大特性

封装:将数据和行为封装在对象内部,提供接口进行访问,隐藏实现细节,提高安全性。

继承:子类可以继承父类的属性和方法,实现代码复用和扩展。

多态:对象可以通过父类或接口进行多态性调用,不同对象在运行时执行不同的行为。

Java方法重载和方法重写之间的区别是什么?

方法重载(Overloading):在同一个类中,允许有多个同名方法,只要它们的参数列表不同(参数个数、类型或顺序)。主要关注方法的签名变化,适用于在同一类中定义不同场景下的行为。

方法重写(Overiding):子类在继承父类时,可以重写父类的某个方法(参数列表、方法名必须相同),从而为该方法提供新的实现。主要关注继承关系,用于子类改变父类的方法实现,实现运行时多态性。

java
public class OverloadingExample {
    // 重载方法:参数数量不同
    public void print(int a) {
        System.out.println("Printing int: " + a);
    }

    // 重载方法:参数类型不同
    public void print(String a) {
        System.out.println("Printing String: " + a);
    }

    // 重载方法:参数类型和数量不同
    public void print(int a, int b) {
        System.out.println("Printing two ints: " + a + ", " + b);
    }
}
java
class Parent {
    public void display() {
        System.out.println("Parent display");
    }
}

class Child extends Parent {
    @Override
    public void display() {
        System.out.println("Child display");
    }
}

public class OverridingExample {
    public static void main(String[] args) {
        Parent obj = new Child();
        obj.display(); // 输出 "Child display"
    }
}

@Override注解,在重写方法时使用@Override注解,编译器可以帮助检查是否正确实现了重写,以防误操作。

什么是Java内部类?它有什么作用?

Java 内部类是指在一个类的内部定义的类,Java支持多种类型的内部类,包括成员内部类、局部内部类、匿名内部类和静态内部类。

内部类可以访问外部类的成员变量和方法,甚至包括私有的成员。

内部类的作用主要包括

  1. 封装性:将逻辑相关的类封装在一起,提高类的内聚性。
  2. 访问外部类成员:内部类可以方便地访问外部类的成员变量和方法,尤其在需要操作外部类对象的场景下非常有用。
  3. 简化代码:对于只在一个地方使用的小类,内部类能减少冗余代码,简化结构。
  4. 事件处理:匿名内部类广泛用于实现回调函数或事件监听,简化了代码结构,特别是对于实现接口或抽象类的场景。

注意

实际上内部类是一个编译层面的概念,像一个语法糖一样,经过编译器之后其实内部类会提升为外部顶级类,和外部类没有任何区别,所以在JVM中是没有内部类的概念的

JDK8有哪些新特性?

  1. Lambda表达式:以简洁的方式来表示函数式接口的实例
  2. StreamAPl:更方便处理集合操作,过滤、排序、分组、映射 操作等。
  3. 接口的默认方法:在接口中使用default定义的方法
  4. 新的日期类API:LocalDateTime、LocalDate、LocalTime等。=>不可变类、线程安全。
  5. 元空间代替永久代:元空间是分配在直接内存中,解决永久代存在的内存不足、GC效率低的问题

Java 中 String、StringBuffer 和 StringBuilder 的区别是什么?

它们都是Java中处理字符串的类,区别主要体现在可变性、线程安全性和性能上:

  1. String

    1. 不可变:String是不可变类,字符串一旦创建,其内容无法更改。每次对string进行修改操作(如拼接、截取等),都会创建新的string 对象。
    2. 适合场景:string适用于字符串内容不会频繁变化的场景,例如少量的字符串拼接操作或字符串常量。
  2. StringBuffer

    1. anxi可变:StringBuffer是可变的,可以进行字符串的追加、删除、插入等操作。
    2. 线程安全:StringBuffer是线程安全的,内部使用了synchronized关键字来保证多线程环境下的安全性。
    3. 适合场景:stringBuffer适用于在多线程环境中需要频繁修改字符串的场景。
  3. StringBuilder

    1. 可变:StringBuilder 也是可变的,提供了与stringBuffer 类似的操作接口。
    2. 非线程安全:StringBuilder 不保证线程安全,性能比 StringBuffer 更高。
    3. 适合场景:StringBuilder适用于单线程环境中需要大量修改字符串的场景,如高频拼接操作

Java的StringBuilder是怎么实现的?

StringBuilder主要是为了解决String对象的不可变性问题,提供高效动态的字符串拼接和修改操作。大致需要实现append、 insert...等功能。

大致核心实现如下:

  1. 内部使用字符数组(char[]value)来存储字符序列
  2. 通过方法(如 append、insert)等操作,直接修改内部的字符数组,而不会像String 那样创建新的对象。
  3. 每次进行字符串操作时,如果当前容量不足,它会通过扩展数组容量来容纳新的字符,按2倍的容量扩展,以减少扩展次数,提高性能。

Java中包装类型和基本类型的区别是什么?

基本类型:Java 中有 8 种基本数据类型(int、long、float、double、char、byte、boolean、short ),它们是直接存储数值的变量,位于栈上(局部变量在栈上、成员变量在堆上、静态(类)字段在方法区),性能较高,且不支持null。

包装类型:每个基本类型都有一个对应的包装类型(Integer、Long、Float、Double、Character 、Byte 、Boolean、Short)。包装类型是类,存储在堆中,可以用于面向对象编程,并且支持null。

区别

  1. 性能区别:
    1. 基本类型:占用内存小,效率高,适合频繁使用的简单操作。
    2. 包装类型:因为是对象,涉及内存分配和垃圾回收,性能相对较低。
  2. 比较方式不同:
    1. 基本类型:比较用==,直接比较数值。
    2. 包装类型:比较时,==比较的是对象的内存地址,而equals()比较的是对象的值。
  3. 默认值不同:
    1. 基本类型:默认值是0,false 等。
    2. 包装类型:默认为 null。
  4. 初始化的方式不同:
    1. 基本类型:直接赋值。
    2. 包装类型:需要采用new的方式创建。
  5. 存储方式不同:
    1. 基本类型:如果是局部变量则保存在栈上面,如果是成员变量则在堆中。
    2. 包装类型:保存在堆上(成员变量,在不考虑JIT优化的栈上分配时,都是随着对象一起保存在堆上的)。

接口和抽象类有什么区别?

接口和抽象类在设计动机上有所不同。

接口的设计是自上而下的。我们知晓某一行为,于是基于这些行为约束定义了接口,一些类需要有这些行为,因此实现对应的接口。

抽象类的设计是自下而上的。我们写了很多类,发现它们之间有共性,有很多代码可以复用,因此将公共逻辑封装成一个抽象类,减少代码冗余。

所谓的自上而下指的是先约定接口,再实现。而自下而上的是先有一些类,才抽象了共同父类。

抽象类

  1. 抽象类不能被实例化
  2. 抽象类应该至少有一个抽象方法
  3. 抽象类的抽象方法没有方法体
  4. 抽象类的子类必须实现父类中的抽象方法,除非子类也是抽象类。

接口

  1. 接口中允许定义变量
  2. 接口中允许定义抽象方法
  3. 接口中允许定义静态方法 (java8之后)
  4. 接口中允许定义默认方法 (java8之后)

接口是协议,调用方无需关心实现细节,实现方维护底层实现,在实现发生变更时,调用方无需感知;抽象类是模版,即一些类共有的部分可以抽象出来,提高代码复用,便于子类扩展。

JDK 和 JRE 有什么区别?

JRE(Java Runtime Environment)指的是Java运行环境,包含了JVM、核心类库和其他支持运行Java程序的文件。

  1. JVM(JavaVirtual Machine):执行Java字节码,提供了Java程序的运行环境。
  2. 核心类库:一组标准的类库(如 java.lang、java.util 等),供Java 程序使用。
  3. 其他文件:如配置文件、库文件等,支持JVM 的运行。

JDK(JavaDevelopmentKit)可以视为JRE的超集,是用于开发Java程序的完整开发环境,它包含了JRE,以及用于开发、调试和监控Java应用程序的工具。

  1. JRE:JDK 包含了完整的JRE,因此它也能运行Java 程序。
  2. 开发工具:如编译器(javac)、调试器(jdb)、打包工具(jar)等,用于开发和管理Java 程序。
  3. 附加库和文件:支持开发、文档生成和其他开发相关的任务。

JDK=JRE+开发工具集(例如 Javac,java 编译工具等)

JRE=JVM+JavaSE标准类库(java核心类库)

你使用过哪些JDK提供的工具?

image-20250206204937887

Java中hashCode和equals方法是什么?它们与==操作符有什么区别?

hashCode、equals和==都是Java中用于比较对象的三种方式,但是它们的用途和实现还是有挺大区别的。

hashCode用于散列存储结构中确定对象的存储位置。可用于快速比较两个对象是否不同,因为如果它们的哈希码不同,那么它们肯定不相等。

equals用于比较两个对象的内容是否相等,通常需要重写自定义比较逻辑。

==用于比较两个引用是否指向同一个对象(即内存地址)。对于基本数据类型,比较它们的值。

Java 中的hashCode 和equals 方法之间有什么关系?

在 Java 中,hashcode()和equals()方法的关系主要体现在集合类(如 HashMap、Hashset)中。

它俩决定了对象的逻辑相等性和哈希存储方式。

  1. equals()方法:

    用于判断两个对象是否相等。默认实现是使用==比较对象的内存地址,但可以在类中重写equals()来定义自己的相等逻辑。

  2. hashCode()方法:

    返回对象的哈希值,主要用于基于哈希的集合(如 HashMapHashset)。同一个对象每次调用 hashCode()必须返回相同的值,且相等的对象必须有相同的哈希码。

如果两个对象根据equals()相等,它们的hashCode()值必须相同。即a.equals(b)== true,那么a.hashCode()==b.hashCode()必须为 true。

但是反过来不要求成立:即两个对象的hashCode()相同,不一定equals()相等。

java
import java.util.Objects;

// 自定义的 Student 类
class Student {
    private String name;
    private int age;

    // 构造函数,用于初始化学生的姓名和年龄
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 重写 hashCode 方法,根据姓名的首字母和年龄计算哈希码
    @Override
    public int hashCode() {
        if (name == null) {
            return age;
        }
        // 取姓名首字母的 ASCII 码加上年龄作为哈希码
        return name.charAt(0) + age;
    }

    // 重写 equals 方法,比较两个 Student 对象是否相等
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        // 当姓名和年龄都相同时,认为两个对象相等
        return age == student.age && Objects.equals(name, student.name);
    }

    public static void main(String[] args) {
        // 创建两个 Student 对象
        Student student1 = new Student("Alice", 20);
        Student student2 = new Student("Amy", 20);

        // 输出两个对象的哈希码
        System.out.println("student1 的哈希码: " + student1.hashCode());
        System.out.println("student2 的哈希码: " + student2.hashCode());

        // 比较两个对象的哈希码是否相同
        boolean hashCodesEqual = student1.hashCode() == student2.hashCode();
        System.out.println("两个对象的哈希码是否相同: " + hashCodesEqual);

        // 比较两个对象是否相等
        boolean areEqual = student1.equals(student2);
        System.out.println("两个对象是否相等: " + areEqual);
    }
}
/***
当运行上述代码时,由于 student1 和 student2 的姓名首字母都是 A 且年龄都是 20,它们的哈希码会相同,但它们的姓名并不完全相同,所以 equals() 方法返回 false。这就展示了两个对象的 hashCode() 相同,但 equals() 不相等的情况。
***/

Java中的注解原理是什么?

注解其实就是一个标记,是一种提供元数据的机制,用于给代码添加说明信息。可以标记在类上、方法上、属性上等,标记自身也可以设置一些值。

注解本身不影响程序的逻辑执行,但可以通过工具或框架来利用这些信息进行特定的处理,如代码生成、编译时检查、运行时处理等。

注解生命周期有三大类,分别是:

  1. RetentionPolicy.SOURCE:给编译器用的,不会写入class文件 比如Override
  2. RetentionPolicy.CLASS:会写入class文件,在JVM加载类阶段丢弃,默认策略
  3. RetentionPolicy.RUNTIME:会写入class文件,永久保存,可以通过反射获取注解信息 比如Autowrite

你使用过Java的反射机制吗?如何应用反射?

Java的反射机制是指在运行时获取类的结构信息(如方法、字段、构造函数)并操作对象的一种机制。反射机制提供了在运行时动态创建对象、调用方法、访问字段等功能,而无需在编译时知道这些类的具体信息。

反射机制的优点

  1. 可以动态地获取类的信息,不需要在编译时就知道类的信息。
  2. 可以动态地创建对象,不需要在编译时就知道对象的类型。
  3. 可以动态地调用对象的属性和方法,在运行时动态地改变对象的行为。

反射在运行状态时可以操作任意一个类的全部功能,比如调用方法、修改成员变量的值和调用构造方法等,就算是私有也可以暴力反射

一般在业务编码中不会用到反射,在框架上用的较多。

什么是Java 的 SPl(Service Provider Interface)机制?

SPI是一种插件机制,用于在运行时动态加载服务的实现。它通过定义接口(服务接口)并提供一种可扩展的方式来让服务的提供者(实现类)在运行时注入,实现解耦和模块化设计。

SPI机制的核心概念

  1. 服务接口:接口或抽象类,定义某个服务的规范或功能。
  2. 服务提供者:实现了服务接口的具体实现类。
  3. 服务加载器(ServiceLoader):Java 提供的工具类,负责动态加载服务的实现类。通过ServiceLoader 可以在运行时发现和加载多个服务提供者。
  4. 配置文件:服务提供者通过在 META-INF/services/目录下配置服务接口的文件来声明自己。这些文件的内容是实现该接口的类的完全限定名。

SPI机制的优势

  1. 解耦:接口与实现分离,客户端不需要依赖具体实现,能够在运行时灵活加载不同的实现类。
  2. 可扩展性:提供了一种易于扩展的机制,允许后期添加或替换实现类,而不需要修改现有代码。

相当于我调用了一个我提供的一个接口,别人去实现这个接口并在模块中写好实现类的配置文件打成jar包,然后由我引入之后就可以使用这个实现类了。和api差不多只是api是接口和实现类都是由第三方提供,spi则是由我提供接口第三方提供实现类。

典型的例子:JDBC

Java泛型的作用是什么?

Java泛型的作用是通过在编译时检查类型安全,允许程序员编写更通用和灵活的代码,避免在运行时发生类型转换错误。

总结

  1. 类型安全:泛型允许在编译时进行类型检查,确保在使用集合或其他泛型类时,不会出现类型不匹配的问题,减少了运行时的ClassCastException错误。
  2. 代码重用:泛型使代码可以适用于多种不同的类型,减少代码重复,提升可读性和维护性。
  3. 消除显式类型转换:泛型允许在编译时指定类型参数,从而消除了运行时需要显式类型转换的麻烦。
java
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0); // 不需要类型转换

Java泛型擦除是什么?

泛型擦除指的是Java编译器在编译时将所有泛型信息删除的过程,以确保与Java1.4及之前的版本保持兼容。

泛型参数在运行时会被替换为其上界(通常是object),这样一来在运行时无法获取泛型的实际类型。

作用:泛型擦除确保了Java代码的向后兼容性,但它也限制了在运行时对泛型类型的操作。

影响:由于类型擦除,无法在运行时获取泛型的实际类型,也不能创建泛型类型的数组或对泛型类型使用instanceof检查。

java
public <T> void printList(List<T> list) {
   for (T element : list) {
       System.out.println(element);
   }
}

在编译时,类型T会被擦除为object,因此编译后的代码类似于:

java
public void printList(List list) {
   for (Object element : list) {
       System.out.println(element);
   }
}

保证了Java的向后兼容性,同时允许泛型与非泛型代码协作。如果没有指定泛型的边界,例如<T>,类型参数T会被替换为Object。<T>直接抹除。如果有边界如<String>,则会被替换成Number,在适当的地方插入强转。

什么是 Java 泛型的上下界限定符?

Java泛型的上下界限定符用于对泛型类型参数进行范围限制,主要有上界限定符(UpperBoundWildcards)和下界限定符(LowerBoundWildcards)。

上界限定符(?extends T)

  1. 定义:?extendsT表示通配符类型必须是T类型或T的子类。
  2. 作用:允许使用T或其子类型作为泛型参数,通常用于读取操作,确保可以读取为T或T的子类的对象。
  3. 示例:
java
public void process(List<? extends Number> list) {
    Number num = list.get(0); // 读取时是安全的,返回类型是 Number 或其子类
    // list.add(1); // 编译错误,不能往其中添加元素
}

下界限定符(?super T)

  1. 定义:? super T表示通配符类型必须是T类型或T的父类。
  2. 作用:允许使用T或其父类型作为泛型参数,通常用于写入操作,确保可以安全地向泛型集合中插入T类型的对象。
  3. 示例:
java
public void addToList(List<? super Integer> list) {
    list.add(1); // 可以安全地添加 Integer 类型的元素
    // Integer value = list.get(0); // 编译错误,不能安全地读取
}

Java中的深拷贝和浅拷贝有什么区别?

深拷贝:深拷贝不仅复制对象本身,还递归复制对象中所有引用的对象。这样新对象与原对象完全独立,修改新对象不会影响到原对象。即包括基本类型和引用类型,堆内的引用对象也会复制一份。

浅拷贝:拷贝只复制对象的引用,而不复制引用指向的实际对象。也就是说,浅拷贝创建一个新对象,但它的字段(若是对象类型)指向的是原对象中的相同内存地址。

深拷贝创建的新对象与原对象完全独立,任何一个对象的修改都不会影响另一个。而修改浅拷贝对象中引用类型的字段会影响到原对象,因为它们共享相同的引用。

什么是Java 的Integer缓存池?

Java的Integer缓存池(lntegerCache)是为了提升性能和节省内存。根据实践发现大部分的数据操作都集中在值比较小的范围,因此缓存这些对象可以减少内存分配和垃圾回收的负担,提升性能。

-128127范围内的Integer 对象会被缓存和复用。

原理

Java在自动装箱时,对于值在-128到127之间的int 类型,会直接返回一个已经缓存的Integer 对象,而不是创建新的对象。

缓存池的使用场景

自动装箱(Auto-boxing):当基本类型int转换为包装类Integer 时,若数值在缓存范围内,返回缓存对象。

比较:由于相同范围内的整数使用同一个缓存对象,使用==可以正确比较它们的地址(引引用相同),而不需要使用equals()。但是要注意对于超过缓存范围的Integer 对象,==比较的是对象引用,而不是数值。要比较数值,应使用equals()方法。

其他

  1. Byte,Short,Integer,Long这4种包装类默认创建了数值[-128,127]的相应类型的缓存数据
  2. Character创建了数值在[0,127]范围的缓存数据
  3. Boolean直接返回True or False
  4. Float和Double没有缓存池,因为是小数,能存的数太多了。

Java 的类加载过程是怎样的?

Java的类加载过程包括加载、链接和初始化三个主要步骤。

  1. 在加载阶段,通过类加载器将类文件加载到内存中,生成一个Class对象。
  2. 在链接阶段,包括验证、准备和解析三个子阶段,确保类的字节码安全并为静态变量分配内存和进行符号引用解析。
  3. 最后在初始化阶段,执行类的初始化逻辑,将静态变量和静态代码块的初始化操作整合并执行。

什么是Java的BigDecimal?

BigDecimal是Java中提供的一个用于高精度计算的类,属于java.math包。它提供对浮点数和定点数的精确控制,特别适用于金融和科学计算等需要高精度的领域。

主要特点

  1. 高精度:BigDecimal可以处理任意精度的数值,而不像float和double存在精度限制。
  2. 不可变性:BigDecimal是不可变类,所有的算术运算都会返回新的BigDecimal对象,而不会修改原有对象(所以要注意性能问题)。
  3. 丰富的功能:提供了加、减、乘、除、取余、舍入、比较等多种方法,并支持各种舍入模式。

BigDecimal为什么能保证精度不丢失?

BigDecimal能够保证精度,是因为它使用了任意精度的整数表示法,而不是浮动的二进制表示。

BigDecimal内部使用两个字段存储数字,一个是整数部分intval,另一个是用来表示小数点的位置scale,避免了浮点数转化过程中可能的精度丢失。

计算时通过整数计算,再结合小数点位置和设置的精度与舍入行为,控制结果精度,避免了由默认浮点数舍入导致的误差。

使用String s = new String("abc")语句在Java中会创建多少个对象?

String s = new String("abc");

  1. 首先,new会先在堆内存中创建一个String对象(第一个对象,称它为new String对象吧),并让s引用指向该对象。
  2. JVM用字面量"abc"去字符串常量池中尝试获取"abc"对应的String对象的引I用。
    1. 如果获取成功,则让newString对象引I用常量池中的"abc"。
    2. 如果获取失败,则在堆内存中创建一个"abc"的String对象(第二个对象),并把它的引用保存在字符串常量池。然后让new String对象引用常量池中的"abc"

Java 中 final、finally和finalize各有什么区别?

  1. final:用于修饰类、方法、和变量,主要用来设计不可变类、确保类的安全性、优化性能(编译器优化)。
    1. 类:被final修饰的类不能被继承。
    2. 方法:被final修饰的方法不能被重写。
    3. 变量:被final修饰的变量不可重新赋值,常用于定义常量。
  2. finally:与try-catch语句块结合使用,用于确保无论是否发生异常,finally代码块都会执行。
  3. finalize():是object类中的方法,允许对象在被垃圾回收前进行清理操作。
    1. 较少使用,JDK9 之后:finalize()方法已被标记为废弃

为什么在Java中编写代码时会遇到乱码问题?

主要原因是字符编码与解码不一致。

xxxxxx示例特点产生原因
古文码鐢辨湀瑕佸ソ濂藉涔犲ぉ澶╁悜涓?大都为不认识的古文,并加杂日韩文以 GBK 方式读取 UTF-8 编码的中文
口字码����Ҫ�¨2�ѧϰ������大部分字符为小方块以 UTF-8 的方式读取 GBK 编码的中文
符号码由月è|å¥½å¥½å-|ä1 天天向上大部分字符为各种符号以 ISO8859-1 方式读取 UTF-8 编码的中文
拼音码óéÔÂòaoÃoÃѧϰììììÏòéÏ大部分字符为头顶带有各种类似声调符号的字母以 ISO8859-1 方式读取 GBK 编码的中文
问句码由月要好好学习天天向??字符串长度为偶数时正确,长度为奇数时最后的字符变为问号以 GBK 方式读取 UTF-8 编码的中文,然后又用 UTF-8 的格式再次读取
锟拷码锟斤拷锟斤拷要锟矫猴拷学习锟斤拷锟斤拷锟斤拷全中文字符,且大部分字符为“锟斤拷”这几个字符以 UTF-8 方式读取 GBK 编码的中文,然后又用 GBK 的格式再次读取
烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫字符显示为“烫烫烫”这几个字符VC Debug 模式下,栈内存未初始化
屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯字符显示为“屯屯屯”这几个字符VC Debug 模式下,堆内存未初始化

为什么 JDK 9 中将 String 的 char 数组改为 byte 数组?

主要是为了节省内存空间,提高内存利用率。

在 JDK 9 之前,String 类是基于char[]实现的,内部采用UTF-16 编码,每个字符占用两个字节。但是,如果当前的字符仅需一个字节的空间,这就造成了浪费。

因此JDK 9做了优化采用byte[]数组来实现,ASCIl字符串(单字节字符)通过byte[]存储,仅需1字节,减小了内存占用。

如果一个线程在Java 中被两次调用start()方法,会发生什么?

会报错.因为在Java中,一个线程只能被启动一次!所以尝试第二次调用start(方法时,会抛出IllegalThreadStateException异常。

这是因为一旦线程已经开始执行,它的状态不能再回到初始状态。线程的生命周期不允许它从终止状态回到可运行状态。

栈和队列在Java中的区别是什么?

栈(Stack):遵循后进先出(LIFO,Last In,First Out)原则。即,最后插入的元素最先被移除。主要操作包括push(入栈)和pop(出栈)。Java 中的Stack类(java.util.stack)实现了这个数据结构。

队列(Queue):遵循先进先出(FIFO,First In,First Out)原则。即,最早插入的元素最先被移除。主要操作包括enqueue(入队)和dequeue(出队)。Java 中的Queue 接口(java.util.Queue)提供了此数据结构的实现,如LinkedList和PriorityQueue 。

使用场景

  1. 栈:常用于函数调用、表达式求值、回溯算法(如深度优先搜索)等场景。
  2. 队列:常用于任务调度、资源管理、数据流处理(如广度优先搜索)等场景。

Java 的Optional 类是什么?它有什么用?

Optional是Java8引入的一个容器类,用于表示可能为空的值。它通过提供更为清晰的APl,来减少程序中出现null的情况,避免 NullPointerExceptionn(空指针异常)的发生。

Optional可以包含一个值,也可以为空,从而表示“值存在”或“值不存在”这两种状态。

作用

  1. 减少NullPointerException:通过Optional提供的操作方法,避免直接使用nul1进行空值检查,从而降低空指针异常的风险。
  2. 提高代码可读性:Optional提供了一套简洁的APl,例如isPresent()、ifPresent()和orElse(),可以让代码更具表达性,清晰地展示处理空值的逻辑。

最后更新于:

Released under the MIT License.