Skip to content

基础概念类

本章内容都由网上整合,以及个人思考,仅用于学习分享,如有侵权部分或者思考错误地方,欢迎大家及时提出,本人一定及时纠正。

分析下面这段代码

java
public class Test {
    public static void main(String[] args) {
        System.out.println(Test.test());
    }
    static int test() {
        int x = 1;
        try {
            return x;
        } finally {
            ++x;
        }
    }
}

运行结果是1。如果 finally 中有代码修改了返回值,finally 里的修改不会影响原来准备返回的值,但它会影响最终的返回行为。

java
public class TestFinally {
    public static void main(String[] args) {
        System.out.println(test());
    }

    public static int test() {
        int x = 1;
        try {
            return x;  // 返回 x 的值(1)
        } finally {
            x = 2;     // 修改 x 的值
            return x;   // 在 finally 中修改了 x 后,返回 x(2)
        }
    }
}

这种就会覆盖

面向对象编程的六大设计原则

1. 单一职责原则(SRP - Single Responsibility Principle)

定义:一个类应该只有一个引起它变化的原因,即一个类应该仅有一个职责。 优点:降低类的复杂度,提高代码的可读性和可维护性。 示例:将数据处理和日志记录分开,不要让同一个类既负责业务逻辑又负责日志输出。

2. 开闭原则(OCP - Open-Closed Principle)

定义:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。 优点:通过增加新功能扩展系统,而不是修改已有代码,减少错误风险。 示例:使用接口或抽象类,使得新增功能可以通过继承或实现接口的方式进行扩展,而无需修改原有代码。

3. 里氏替换原则(LSP - Liskov Substitution Principle)

定义:子类对象应该能够替换其父类对象,并且程序的行为不会改变。 优点:保证继承的正确性,避免因子类修改父类行为而导致意外问题。 示例:如果一个基类 Birdfly() 方法,那么 Penguin 不能继承 Bird,因为企鹅不会飞。

4. 接口隔离原则(ISP - Interface Segregation Principle)

定义:不应强迫一个类实现它用不到的接口,即接口应该小而精,不要定义臃肿的大接口。 优点:避免不必要的依赖,提高代码的灵活性和可维护性。 示例:比起设计一个 Animal 接口包含 run()fly() 方法,不如拆分成 RunnableFlyable 接口,分别适用于不同的动物。

5. 依赖倒置原则(DIP - Dependency Inversion Principle)

定义:高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于具体实现,具体实现应该依赖于抽象。 优点:降低模块之间的耦合度,提高系统的稳定性和可扩展性。 示例:不要在类中直接实例化一个具体对象,而是通过依赖注入(DI)提供接口或抽象类,从而使得底层实现可以自由变化。

6. 迪米特法则(LoD - Law of Demeter)

定义:一个对象应该尽量少地了解其他对象,即“最少知识原则”。 优点:降低类之间的耦合,提高模块的独立性。 示例:避免“链式调用”,例如 user.getAddress().getCity().getZipCode(),应当改为 user.getZipCode()User 直接提供必要信息。

数组是不是对象

java
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Class clz = int[].class;
        System.out.println(clz.getSuperclass().getName());
        int[] arr = new int[5];
	    System.out.println(arr instanceof Object); // true
    }
}
------output------
java.lang.Object

数组既是对象,但又有特殊的性质:

1. 数组是对象

  • 在 Java 中,数组是对象,存储在堆(Heap)中,可以通过 getClass() 获取其运行时类型。

  • 可以使用 instanceof 进行检查,例如:

    java
    int[] arr = new int[5];
    System.out.println(arr instanceof Object); // true
  • 数组的 toString() 方法没有被重写,直接打印数组时会输出类似 [I@1b6d3586,而不是数组内容。

2. 数组有固定的类型

  • Java 的数组是强类型的,一旦声明了类型,就不能存储其他类型的数据:

    java
    String[] strArray = new String[3];
    strArray[0] = "Hello"; // 正确
    strArray[1] = 123;      // 错误,类型不匹配

3. 数组继承自 Object,但没有显式的类

  • Java 数组没有显式的类定义,但它

    继承自 Object,因此可以调用 hashCode() equals()等方法:

    java
    int[] arr = new int[5];
    System.out.println(arr.getClass().getName()); // [I 表示int数组

结论

在 Java 中,数组本质上是对象,但它是一个特殊的对象,具有固定的类型,并受 Java 语言规则约束。

Java泛型和类型擦除

类型擦除 是Java泛型的一个重要特性,Java编译器在编译时会擦除所有泛型类型信息,将泛型代码转换成普通的非泛型代码。这是为了保持 Java 的兼容性,确保泛型引入时不会影响到现有的代码。

  1. 泛型参数被替换为 Object

    java
    // 泛型类
    List<String> list = new ArrayList<>();
    
    // 类型擦除后,变成
    List list = new ArrayList();
  2. 删除泛型的所有类型参数

    java
    // 泛型方法
    public <T> void print(T item) { 
        System.out.println(item);
    }
    
    // 类型擦除后,变成
    public void print(Object item) {
        System.out.println(item);
    }
  3. 泛型的边界约束被擦除

    java
    public <T extends Number> void print(T item) { 
        System.out.println(item);
    }
    
    // 类型擦除后,变成
    public void print(Number item) {
        System.out.println(item);
    }
  4. 方法重载问题

    java
    public void printList(List<String> list) {
        System.out.println("String List");
    }
    
    public void printList(List<Integer> list) {
        System.out.println("Integer List");
    }
    // 编译时错误,无法区分这两个方法,因为类型擦除后它们都变成了 List

类型擦除的影响

  • 安全性:类型擦除使得 Java 在运行时无法获取泛型类型的信息,这意味着泛型不能用于实例化新的对象或者执行反射等操作。
  • 性能:由于在编译时会进行类型擦除,程序的运行时性能不会受到影响。
  • 类型检查:虽然泛型的类型信息会被擦除,但编译器在编译时会进行类型检查,以确保类型安全。

hashCode() 和 equals()

hashCode()equals() 的关系

  • hashCode():返回对象的哈希值,用于确定对象在哈希表中的位置。它是一个整数值,用来表示对象的哈希码(可以理解为对象的内存地址或某种形式的唯一标识)。
  • equals():用来判断两个对象是否相等。默认情况下,Object 类中的 equals() 方法比较的是对象的内存地址(即引用相等),但通常我们会根据业务需求重写这个方法来比较对象的内容是否相等。

为什么重写 equals() 时要重写 hashCode() 方法?

  • 如果两个对象通过 equals() 方法比较相等(即 obj1.equals(obj2) 返回 true),那么这两个对象的 hashCode() 必须相同。
  • 但是,反之则不成立,即如果两个对象的 hashCode() 相同,它们不一定是相等的。

HashSetHashMap 等基于哈希表的集合会通过 hashCode() 方法来决定对象存储的位置(桶的索引)。如果我们没有正确重写 hashCode() 方法,即使我们重写了 equals(),也可能会出现两个内容相同的对象在哈希表中被认为不相等的情况,从而导致错误的行为。

举个例子:

java
class Person {
    private String name;
    private int age;

    // Constructor, getters, setters

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return age == person.age && name.equals(person.name);
    }

    // hashCode() method
    /*
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
    */
}

Set<Person> set = new HashSet<>();
Person p1 = new Person("Alice", 30);
Person p2 = new Person("Alice", 30);
set.add(p1);
set.add(p2);

System.out.println(set.size());  // 实际输出会是2,这和我们预想的1不一样

原因:

  • p1p2 是内容相等的对象(名字和年龄相同),所以调用 equals() 时应该返回 true

  • 但是,由于我们没有重写 hashCode(),默认使用 Object 类的 hashCode() 方法。hashCode() 方法的默认实现是基于对象的内存地址,因此 p1p2 的哈希值可能不同。

  • 结果,HashSet 会认为 p1p2 是不同的对象,并且它们都被添加到了集合中,导致集合的大小为 2,而不是 1。

Java的基础类型有哪些

Java 中有 8 种基本数据类型,分别为:

  • 6 种数字类型:
    • 4 种整数型:byteshortintlong
    • 2 种浮点型:floatdouble
  • 1 种字符类型:char
  • 1 种布尔型:boolean

这 8 种基本数据类型的默认值以及所占空间的大小如下:

基本类型位数字节默认值取值范围
byte810-128 ~ 127
short1620-32768(-2^15) ~ 32767(2^15 - 1)
int3240-2147483648 ~ 2147483647
long6480L-9223372036854775808(-2^63) ~ 9223372036854775807(2^63 -1)
char162'u0000'0 ~ 65535(2^16 - 1)
float3240f1.4E-45 ~ 3.4028235E38
double6480d4.9E-324 ~ 1.7976931348623157E308
boolean1falsetrue、false

注意:

  1. Java 里使用 long 类型的数据一定要在数值后面加上 L,否则将作为整型解析。
  2. Java 里使用 float 类型的数据一定要在数值后面加上 f 或 F,否则将无法通过编译。
  3. char a = 'h'char :单引号,String a = "hello" :双引号。

基础类型和包装类型的区别

用途:除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以。

存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。

占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。

默认值:成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null

比较方式:对于基本数据类型来说,== 比较的是值。对于包装数据类型来说,== 比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 equals() 方法。

为什么说是几乎所有对象实例都存在于堆中呢? 这是因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存

⚠️ 注意:基本数据类型存放在栈中是一个常见的误区! 基本数据类型的存储位置取决于它们的作用域和声明方式。如果它们是局部变量,那么它们会存放在栈中;如果它们是成员变量,那么它们会存放在堆/方法区/元空间中。

包装类型的缓存机制了解吗?

Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。

Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False

java
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    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;
    }
}

如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。

两种浮点数类型的包装类 Float,Double 并没有实现缓存机制。

下面我们来看一个问题:下面的代码的输出结果是 true 还是 false 呢?

java
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);

Integer i1=40 这一行代码会发生装箱,也就是说这行代码等价于 Integer i1=Integer.valueOf(40) 。因此,i1 直接使用的是缓存中的对象。而Integer i2 = new Integer(40) 会直接创建新的对象。

因此,答案是 false 。你答对了吗?

记住:所有整型包装类对象之间值的比较,全部使用 equals 方法比较

如何解决浮点数运算的精度丢失问题

BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。

成员变量与局部变量的区别

  • 语法形式:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
  • 存储方式:从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
  • 生存时间:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
  • 默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

为何JDK9要将String的底层实现由char[]改成byte[]?

1. 减少内存占用

JDK 8 及以前:char[] 存储

  • char 类型在 Java 中占 2 个字节(因为 Java 采用 UTF-16 编码)。
  • 即使字符串中全是 ASCII 字符(如 "hello"),每个字符仍然占 2 个字节,导致 额外的内存浪费

JDK 9 及以后:byte[] 存储

  • Java 9 引入了紧凑字符串(Compact Strings),用 byte[] 存储字符串:
    • 如果字符串只包含 Latin-1(ISO-8859-1,单字节编码) 字符,则每个字符仅占 1 个字节(节省 50% 空间)。
    • 如果包含 非 Latin-1 字符(如中文、日文),则仍然使用 UTF-16(2 字节存储)

示例

  • JDK 8(char[])"hello" 需要 10 字节(5 个 char × 2 字节)。
  • JDK 9(byte[])"hello" 只需要 5 字节(全部是 Latin-1 字符)。

2. 提高 CPU 缓存命中率

  • byte[] char[] 结构紧凑,减少了不必要的填充和对齐,提高了 CPU 缓存利用率。
  • 在处理大量字符串时,减少内存访问,提高了 GC(垃圾回收)效率

3. String 操作性能提升

  • 由于占用更少的内存,字符串操作(如拼接、复制、子串提取)效率更高
  • Stringsubstring() 方法不再共享原数组,避免 JDK 6/7 时代的 内存泄漏问题

4. String 仍然是不可变的

  • 尽管底层从 char[] 变成 byte[],但 String 仍然不可变(final)。

  • String 额外维护了一个 coder 字段:

    java
    private final byte[] value;  // 存储字符串数据
    private final byte coder;    // 编码方式(0 = Latin-1, 1 = UTF-16)
    • Latin-1(1 字节/字符)coder = 0
    • UTF-16(2 字节/字符)coder = 1

String最大长度是多少?

String类提供了一个length方法,返回值为int类型,而int的取值上限为2^31 -1。

java
public int length() {
    return value.length >> coder();
}

既然返回的是 int 类型,所以理论上String的最大长度为 2^31 -1。但是我们还需要考虑到String也有可能存放在字符串常量池中,通过字面量进行字符串声明时,编译后会以常量的形式进入到常量池。常量池中length的类型是u2,无符号16位整数,所以最大长度是65534

String s = new String("xyz");创建了几个String Object? 二者之间有什么区别?

两个或一个,”xyz”对应一个对象,这个对象放在字符串常量缓冲区,常量”xyz”不管出现多少遍,都是缓冲区中的那一个。New String每写一遍,就创建一个新的对象,它一句那个常量”xyz”对象的内容来创建出一个新String对象。如果以前就用过’xyz’,这句代表就不会创建”xyz”自己了,直接从缓冲区拿。

String \ StringBuilder \ StringBuffer 的区别

  • 不可变性

    • String内部的字符数组使用 final修饰,为不可变的字符串类,每次对String对象进行改变时,实际都会创建一个新的String对象,旧对象会被JVM收回,容易触发gc,引起系统内存抖动

      java
      // 以下为部分源码,供参考
      public final class String
          implements java.io.Serializable, Comparable<String>, CharSequence,
                     Constable, ConstantDesc {
      
      /**
      
       * The value is used for character storage.
         *
       * @implNote This field is trusted by the VM, and is a subject to
       * constant folding if String instance is constant. Overwriting this
       * field after construction will cause problems.
         *
       * Additionally, it is marked with {@link Stable} to trust the contents
       * of the array. No other facility in JDK provides this functionality (yet).
       * {@link Stable} is safe here, because value is never null.
         */
         @Stable
         private final byte[] value;
    • StringBuilderStringBuffer 是可变的,

  • 线程安全

    • String:由于不可变,所以线程安全
    • StringBuffer:方法都使用 synchronized 关键词修饰,线程安全
    • StringBuilder:线程不安全
  • 性能StringBuilderStringBuffer 效率高于 String

  • 使用场景

    • String:常量的声明等字符串不经常变化的场景
    • StringBuilder:运行在单线程环境中、频繁进行字符串运算(如拼接、替换、删除等),如SQL语句的拼装,JSON封装等
    • StringBuffer:运行在多线程环境中、频繁进行字符串运算(如拼接、替换、删除等),如 XML 解析、HTTP 参数解析和封装

    再附上StringBuilder和StringBuffer的部分源码

    java
    // 由于StringBuilder和StringBuffer都继承这个抽象类,我们直接参考这个即可
    // 至于线程安全,感兴趣的朋友可以自行去源码查看,StringBuffer下的方法都带有synchronized,而StringBuilder是没有的
    abstract sealed class AbstractStringBuilder implements Appendable, CharSequence
        permits StringBuilder, StringBuffer {
        /**
         * The value is used for character storage.
         */
        byte[] value;

接口和抽象类的区别

  1. 定义:
    • 接口是一种抽象类型,只能包含常量(static final变量)和抽象方法
    • 抽象类是一个类,可以包含抽象方法和具体方法
  2. 继承:
    • 接口支持多继承
    • Java中不支持多继承类,一个类只能继承一个抽象类
  3. 构造器:
    • 接口不能包含构造器,因为接口不能实例化
    • 抽象类可以包含构造器
  4. 访问修饰符:
    • 接口的方法默认是public abstract,变量默认是public static final
    • 抽象类中的抽象方法默认是 protected的,具体方法的访问修饰符可以是publicprotectedprivate
  5. 设计目的:
    • 接口用于定义规范,强调“行为”或“能力“
    • 抽象类用于代码复用,提供通用的实现或者基础功能

常见的异常

  • Java 的异常都派生于 Throwable 类的一个实例,所有异常都是由 Throwable 继承来的

  • 异常分为 RuntimeException 和其他异常:

    • 由程序错误导致的异常属于 RuntimeException
    • 而程序本身没有问题,但由于像 I/O 错误这类问题导致的异常属于其他异常
  • 运行时异常 RuntimeException

    运行时才可能抛出的异常,编译器不会处理这类异常。比如数组索引越界 ArrayIndexOutOfBoundsException、使用的对象为空 NullPointException、强制类型转换错误 NullPointException 等等。

  • 其他异常:

    Exception 中除了运行时异常之外,都属于其他异常。也成为编译时异常,这部分异常编译器要求必须处置。这部分异常通常是因为外部运行环境导致,例如打开一个不存在的文件,此时抛出异常 FileNotFoundException

Exception 和 Error 有什么区别

ExceptionError 都是派生自 Throwable 类的子类

  1. Exception 类及其子类表示程序可以处理的异常情况。异常分为两种类型:可检查异常(Checked Exception)和不可检查异常(Unchecked Exception)。程序员可以选择捕获并处理异常,也可以通过在方法签名中使用 throws 关键字声明方法可能抛出的异常。

    补充:

    • 可检查异常:在编译时由编译器检查的异常,程序员必须显示处理或声明这些异常。如果不处理,编译器将报错。通常派生自 Exception 类及其子类,但不派生自 RuntimeException
    • 不可检查异常是在运行时才会被检测到的异常,通常是由编程错误导致的。这些异常通常派生自 RuntimeException 或其子类。程序员可以选择处理这些异常。
  2. Error

    • Error 类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。
    • Error 表示比较严重的问题,一般是 JVM 运行时出现了错误,如没有内存可分配抛出 OOMOut Of Memory)错误、栈资源耗尽抛出 StackOverflowError 错误等等
    • 这些错误的发生通常意味着程序无法继续正常执行。如果出现了这样的错误,除了通告给用户,并尽力使程序安全地终止之外,再也无能为力了。

重载和重写的区别

  1. 重载是在同一个类中定义多个方法,方法名相同但参数列表不同
  2. 重写是在子类中重新定义父类中已有的方法,方法名和参数列表必须相同
  3. 重载与返回类型和访问修饰符无关,而重写要求方法签名相同
  4. 重载是编译时多态,重写是运行时多态

final关键词的理解

final可以修饰变量、方法、类

  1. 修饰类: final 修饰的类不可被继承,是最终类
  2. 修饰方法: 明确禁止该方法在子类中被覆盖
  3. 修饰变量:
    • final 修饰 基本数据类型的变量,其数值初始化后就不能修改,成为常量
    • final 修饰 引用类型的变量,初始化后就不能再让其指向另一个对象,但是指向的对象的内容是可变的

浅拷贝和深拷贝

在 Java 中,浅拷贝(Shallow Copy) 的本质是:

复制对象本身(即创建一个新对象),但不复制对象内部的引用类型字段,而是共享它们的引用地址。

Object 类提供了 clone() 方法,默认实现是按位复制对象,但它仅复制基本数据类型和引用地址,而不会复制引用对象的内容。我们用这个来说明浅拷贝

java
import java.util.ArrayList;
import java.util.List;

public class Cat implements Cloneable{
    private String name;
    private List<String> hobbies = new ArrayList<>();

    @Override
    protected Cat clone() throws CloneNotSupportedException {
        return (Cat) super.clone();
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public List<String> getHobbies() {
        return hobbies;
    }
    public void setHobbies(List<String> hobbies) {
        this.hobbies = hobbies;
    }
}

public static void main(String[] args) throws InterruptedException, CloneNotSupportedException {
        Cat cat = new Cat();
        cat.setName("程序员小猪");
        cat.getHobbies().add("睡觉");
        Cat cloneCat = (Cat) cat.clone();
        cloneCat.setName("Slxner");
        cloneCat.getHobbies().add("吃肉");
        System.out.println(cat == cloneCat); // false
        System.out.println(cat.getHobbies()); // [睡觉, 吃肉]
        System.out.println(cloneCat.getHobbies()); // [睡觉, 吃肉]
    }

说明 hobbies 仍然指向同一个 List,修改 cloneCathobbiescat 也会受到影响。

这就是浅拷贝——仅复制对象本身,而对象内部的引用类型属性仍然指向同一个地址。

那么如何修改做到深拷贝呢

java
@Override
protected Object clone() throws CloneNotSupportedException {
    Cat cloned = (Cat) super.clone();
    cloned.hobbies = new ArrayList<>(this.hobbies); // 手动复制列表
    return cloned;
}

深拷贝(Deep Copy)指的是 不仅复制对象本身,还递归复制对象内部所有的引用对象,使得拷贝对象与原对象完全独立,二者不会共享任何可变的引用数据

一句话理解:深拷贝就是创造一个真正的“独立副本”,无论修改原对象还是拷贝对象,都不会影响对方。

谈一谈对泛型的理解

  • 拿Java中的集合举例,在JDK1.5引入泛型机制之前,集合存在一个缺点,当你将对象存入集合中,集合会失去对该对象的具体数据类型的记忆,导致取出对象时,集合将其视为 Object类型。举个例子,我们有一个Dog类和Cat类,当我们想创建一个只存放Dog类的集合时,会发现我们把Cat类放入其中也是没有问题的。

    java
    List animals = new ArrayList();
    animals.add(new Dog());
    animals.add(new Cat());

    在这种情况下,集合对存储的对象类型没有限制,任何对象都可以添加进去。这就需要在取出元素时进行类型转换:如果不小心将Cat对象转换为Dog,会在运行时抛出ClassCastException

    java
    Dog dog = (Dog) animals.get(0);

    起源是因为集合的设计者在创建时无法确定集合将会被用来存储哪种类型的对象。为了解决.

  • 引入泛型后,可以在定义集合时指定其存储的类型:

    java
    List<Dog> dogs = new ArrayList<>();
    dogs.add(new Dog());
    // dogs.add(new Cat()); // 编译时会报错
  • 泛型的优势

    • 类型安全:在编译时检查类型,避免了运行时的类型转换异常。
    • 代码可读性:明确集合中存储的对象类型,增强代码的可读性和维护性。
  • 类型擦除

    类型擦除的实现过程

    1. 替换类型参数:编译器将泛型类型参数替换为其上界或Object
    2. 插入类型转换:在需要返回泛型类型的地方,编译器会插入强制类型转换,以确保类型安全。
    3. 生成桥接方法:对于涉及泛型的继承或实现关系,编译器可能生成桥接方法(bridge methods)以确保多态性。

    类型擦除的影响

    • 运行时类型信息缺失:由于类型擦除,泛型类型参数在运行时不可获取,这意味着无法在运行时判断或获取泛型的实际类型参数。
    • 与非泛型代码兼容:类型擦除确保了泛型代码与非泛型代码的兼容性,使得引入泛型后无需更改现有的非泛型代码。

更多相关可以查看Java语言规范官网

面向对象和面向过程有什么区别吗

  • 面向对象:抽象出具有状态和行为的对象,以对象执行方法的方式解决方法,强调封装继承多态(这三个特性也是 Java 的三大特性),更容易扩展和维护,不会因为修改一个对象而影响到另一个对象。适合处理复杂的系统
  • 面向过程:强调的是算法和流程,如果需要修改某一个过程,可能会影响到整体流程的进行,适合简单的、线性的任务。

多态的实现

首先,多态是指同一操作作用于不同的对象,可以有不同的行为。在Java中,多态性通过方法的重载和重写来实现。这允许一个方法在不同的类中具有不同的实现。

多态有两种形式:编译时多态(静态多态)和运行时多态(动态多态)。

  • 编译时多态: 通过方法的重载实现。编译器在编译时根据方法的参数数量、类型或顺序来选择调用合适的方法。

    java
    public class Example {
        public int add(int a, int b) {
            return a + b;
        }
        public double add(double a, double b) {
            return a + b;
        }
        public static void main(String[] args){
            Example example = new Example();
            int resultInt = example.add(1, 2);
            double resultDouble = example.add(2.0, 3.0);
        }
    }
  • 运行时多态: 通过方法的重写实现。在运行时,通过对象的实际类型来确定调用哪个版本的方法。

    java
    class Parent {
        void show() { System.out.println("Parent show"); }
    }
    class Child extends Parent {
        @Override
        void show() { System.out.println("Child show"); }
    }
    public class Test {
        public static void main(String[] args) {
            Parent obj = new Child();  // 向上转型
            obj.show();  // 运行时决定调用 Child 的 show()
        }
    }

    在编译时,obj 只是一个 Parent 类型的引用,但在运行时,它指向 Child 的实例,因此 调用的是 Childshow() 方法。这就是运行时多态