Java基础知识点汇总(四)

一、String 有哪些特性?

在Java中,String 是最常用的类之一,具有以下核心特性:

  1. 不可变性(Immutable)
  • 定义:字符串对象一旦创建,其内容(字符序列)就无法被修改。任何看似修改字符串的操作(如拼接、替换)都会创建新的 String 对象,原对象保持不变。
  • 示例
    String s = "hello";
    s += " world"; // 原"hello"对象未被修改,而是创建了新对象"hello world"
    
  • 好处
    • 线程安全:不可变对象可安全地在多线程中共享,无需同步。
    • 可缓存哈希值:StringhashCode() 计算结果会被缓存,多次调用无需重复计算,提升哈希表(如 HashMap)效率。
    • 安全性:避免字符串被意外修改(如网络请求参数、文件路径等)。
      • 使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。
  1. 字符串常量池(String Constant Pool)
  • 定义:JVM为 String 维护的一块特殊内存区域,用于存储字符串常量(编译期确定的字符串),避免重复创建相同内容的字符串对象,节省内存。
  • 工作机制
    • 直接赋值(String s = "abc"):优先从常量池查找,若存在则直接引用,否则创建新对象并放入常量池。
    • new 关键字创建(String s = new String("abc")):会在堆中创建新对象,同时常量池中若不存在"abc"也会创建(但变量s指向堆对象)。
  • 示例
    String s1 = "abc";
    String s2 = "abc";
    String s3 = new String("abc");
    
    System.out.println(s1 == s2); // true(均指向常量池同一对象)
    System.out.println(s1 == s3); // false(s3指向堆中对象)
    
  1. 值传递特性(表现类似基本类型)
  • String 是引用类型,但因不可变性,其传递行为类似基本类型:方法内部对字符串参数的修改不会影响原对象。
  • 示例
    public static void main(String[] args) {
        String s = "hello";
        change(s);
        System.out.println(s); // 输出"hello"(原对象未变)
    }
    
    public static void change(String str) {
        str = "world"; // 创建新对象,不影响原引用
    }
    
  1. 常用方法丰富
    String 类提供了大量实用方法,方便字符串操作:
  • 查找:indexOf()lastIndexOf()contains()
  • 截取:substring()
  • 转换:toUpperCase()toLowerCase()valueOf()(将其他类型转为字符串)
  • 替换:replace()replaceAll()
  • 分割:split()
  • 去除空格:trim()(JDK 11+ 可用 strip() 更完善处理 Unicode 空格)
  1. 实现了关键接口
  • Serializable:支持序列化,可在网络传输或持久化存储。
  • Comparable<String>:重写了 compareTo() 方法,支持自然排序(按字符Unicode值比较)。
  • CharSequence:表示字符序列,与 StringBuilderStringBuffer 兼容。
  1. 与 StringBuilder、StringBuffer 的区别
  • String 不可变,适合少量字符串操作或常量定义。
  • StringBuilder 可变,线程不安全,效率高(单线程推荐)。
  • StringBuffer 可变,线程安全(有同步锁),效率较低(多线程场景用)。

总结:
String 的核心特性是不可变性常量池机制,这使得它在安全性、性能和便捷性之间取得了很好的平衡。理解这些特性有助于写出更高效、更安全的Java代码(例如,避免在循环中频繁拼接字符串,应改用 StringBuilder)。

二、String、StringBuffer、StringBuilder 区别?

在Java中,StringStringBufferStringBuilder 都用于处理字符串,但它们在可变性、线程安全性和性能上有显著区别,适用场景也不同:

  1. 可变性
  • String不可变(Immutable)
    字符串对象一旦创建,其内容(字符序列)无法被修改。任何修改操作(如拼接、替换)都会创建新的 String 对象,原对象保持不变。

    String s = "a";
    s += "b"; // 原对象"a"不变,创建新对象"ab"
    
  • StringBufferStringBuilder可变(Mutable)
    它们的内部字符序列可以直接修改,不会创建新对象。修改操作(如 append()insert())是在原对象上进行的。

    StringBuilder sb = new StringBuilder("a");
    sb.append("b"); // 直接在原对象上修改,结果为"ab"
    
  1. 线程安全性
  • String线程安全
    由于不可变性,多线程环境下可以安全地共享 String 对象,无需额外同步。

  • StringBuffer线程安全
    其方法被 synchronized 关键字修饰(同步方法),多线程操作时不会出现数据不一致问题,但会带来性能开销。

  • StringBuilder线程不安全
    方法没有同步机制,多线程同时修改可能导致数据错乱,但性能更高。

  1. 性能
  • String性能较低
    频繁修改字符串(如循环拼接)会产生大量临时对象,导致内存浪费和GC(垃圾回收)压力,效率低下。

  • StringBuffer性能中等
    因同步机制,单线程环境下性能不如 StringBuilder,但比 String 高效(尤其频繁修改时)。

  • StringBuilder性能最高
    无同步开销,单线程中大量字符串操作时,效率远高于 StringStringBuffer

  1. 适用场景
适用场景典型操作示例
String字符串内容不变(如常量定义、少量拼接)定义固定文本、参数传递
StringBuffer多线程环境下的字符串修改(如并发场景)多线程日志拼接、网络通信处理
StringBuilder单线程环境下的大量字符串修改(最常用)循环拼接、动态生成SQL/JSON字符串

示例对比

// String:多次拼接产生多个对象,效率低
String str = "";
for (int i = 0; i < 1000; i++) {
    str += i; // 每次循环创建新对象
}

// StringBuffer:线程安全,单线程下稍慢
StringBuffer sb1 = new StringBuffer();
for (int i = 0; i < 1000; i++) {
    sb1.append(i);
}

// StringBuilder:单线程下最快,推荐使用
StringBuilder sb2 = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb2.append(i);
}

总结:

  • 不可变 vs 可变String 不可变,StringBuffer/StringBuilder 可变。
  • 线程安全StringStringBuffer 安全,StringBuilder 不安全。
  • 性能StringBuilder > StringBuffer > String(频繁修改时)。

日常开发中,单线程场景优先用 StringBuilder,多线程场景用 StringBuffer,字符串不变时用 String

三、HashMap 用 String 做 key 有什么好处?

在 HashMap 中使用 String 作为 key 有多个显著优势,这些优势源于 String 类的特性与 HashMap 的设计原理高度契合:

  1. String 是不可变的(Immutable)
  • 核心优势:String 对象的哈希值(hashCode())在创建时就已确定,且不会因内容修改而变化(因为内容无法修改)。
  • HashMap 角度:HashMap 依赖 key 的哈希值确定存储位置。如果 key 是可变对象,修改后哈希值变化会导致:
    • 原位置找不到该 key(无法正确查询)。
    • 新位置可能重复存储,破坏 HashMap 结构。
  • String 保证:由于不可变,哈希值始终稳定,确保 key 在 HashMap 中的存储位置不变,查询和操作的正确性得到保障。
  1. 哈希值计算高效且缓存
  • String 的 hashCode() 优化
    • String 类重写了 hashCode(),基于字符序列计算(s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]),算法高效。
    • 哈希值会被缓存到内部变量 hash 中,首次计算后无需重复计算,多次调用 hashCode() 时性能极高。
  • HashMap 受益:减少哈希值计算的开销,提升 HashMap 的插入、查询效率。
  1. equals() 方法逻辑清晰
  • String 重写了 equals() 方法,通过逐个比较字符序列判断相等性,逻辑明确且严格遵循“相等的对象必须有相等哈希值”的约定。
  • 这与 HashMap 中“先通过哈希值定位,再用 equals() 精确比对”的工作机制完美匹配,确保正确识别相同的 key。
  1. 字符串常量池优化内存
  • String 存在字符串常量池,相同内容的字符串常量会复用同一个对象(如 String a = "key"; String b = "key";ab 指向同一对象)。
  • 在 HashMap 中使用这类字符串作为 key 时,会减少 key 对象的创建,节省内存空间。
  1. 使用便捷,语义明确
  • 字符串是最自然的“键”类型(如配置项 key、字典键等),可读性强,使用时无需额外定义自定义类作为 key(避免重写 hashCode()equals() 的麻烦)。

示例对比:

// 使用 String 作为 key(推荐)
HashMap<String, Integer> map = new HashMap<>();
map.put("name", 1);
map.get("name"); // 高效且安全

// 使用自定义可变对象作为 key(需谨慎)
class MutableKey {
    private String value;
    // 省略构造方法、getter/setter
    
    @Override
    public int hashCode() { return value.hashCode(); }
    @Override
    public boolean equals(Object o) { /* 基于 value 比较 */ }
}

MutableKey key = new MutableKey("key");
map.put(key, 1);
key.setValue("newKey"); // 修改 key 内容,导致哈希值变化
map.get(key); // 无法找到对应值,出现逻辑错误

总结:
String 作为 HashMap 的 key 时,不可变性保证了哈希值稳定优化的 hashCode() 和 equals() 提升了效率常量池减少了内存占用,同时兼具便捷性和语义清晰性。这些特性使其成为 HashMap 中最常用、最推荐的 key 类型之一。

四、try-catch-finally,如果 catch 中 return了,还会执行finally吗?

在 Java 中,即使 catch 块中使用了 return 语句,finally仍然会执行,且执行时机是在 catch 块的 return 语句之前。

执行顺序说明:

  1. try 块中发生异常,程序进入 catch 块。
  2. 执行 catch 块中的代码,直到遇到 return 语句。
  3. 在执行 return 之前,程序会先跳转到 finally 块执行所有代码。
  4. finally 块执行完毕后,再回到 catch 块中执行 return 语句,结束方法。

示例验证:

public class TryCatchFinallyDemo {
    public static int test() {
        try {
            int i = 1 / 0; // 触发异常,进入catch
            return 1;
        } catch (ArithmeticException e) {
            System.out.println("进入catch块");
            return 2; // catch中return
        } finally {
            System.out.println("执行finally块"); // 仍然会执行
        }
    }

    public static void main(String[] args) {
        System.out.println("结果:" + test());
    }
}

输出结果:

进入catch块
执行finally块
结果:2

从输出可见,finally 块在 catchreturn 之前被执行了。

特殊情况:如果 finally 中也有 return?
如果 finally 块中也存在 return 语句,会覆盖 catch 块的 return 结果,且方法会直接从 finallyreturn 退出(不再回到 catch 块)。

示例:

public static int test() {
    try {
        int i = 1 / 0;
        return 1;
    } catch (Exception e) {
        return 2; // 此return会被finally覆盖
    } finally {
        return 3; // 最终返回此值
    }
}
// 调用test()的结果为:3

结论:

  • finally 块始终会执行(除非在 try/catch 中调用了 System.exit(0) 强制退出虚拟机)。
  • 即使 catch 中有 returnfinally 也会在 return 之前执行。
  • 不建议在 finally 中使用 return,会破坏程序逻辑的可读性,且可能覆盖预期返回值。

finally 的设计初衷就是保证无论是否发生异常,都能执行必要的清理操作(如关闭流、释放资源等),因此它的执行优先级很高。

五、Error 和 Exception 区别?

在Java中,ErrorException都是Throwable类的子类,用于表示程序运行时的异常情况,但它们的含义、使用场景和处理方式有本质区别:

  1. 本质含义与设计目的
  • Error(错误)
    表示JVM层面的严重问题,通常是程序无法处理的底层错误,源于系统级别的故障或资源耗尽。
    例如:

    • OutOfMemoryError:内存溢出(JVM无法分配足够内存)。
    • StackOverflowError:栈溢出(递归调用过深等导致)。
    • NoClassDefFoundError:类定义未找到(类文件缺失或加载失败)。
  • Exception(异常)
    表示程序运行时的预期内错误,通常是由代码逻辑缺陷或外部环境异常导致,程序应该(且能够)处理这类问题。
    例如:

    • NullPointerException:空指针访问(代码逻辑错误)。
    • IOException:输入输出异常(文件不存在、网络中断等)。
    • ClassCastException:类型转换异常(错误的类型转换逻辑)。
  1. 处理方式
  • Error
    程序不应该捕获和处理(通常也无法处理)。这类错误发生时,JVM通常会终止线程甚至退出,开发者能做的是提前规避(如优化内存使用、避免无限递归)。

  • Exception
    程序应该捕获并处理(通过try-catch),或在方法上声明抛出(throws)由上层调用者处理。目的是通过异常处理机制使程序从错误中恢复,继续执行。

  1. 分类与层级
  • Error
    属于Throwable的直接子类,常见子类均为严重错误(如上述例子),通常不需要自定义Error子类。

  • Exception
    分为两大类:

    1. 受检异常(Checked Exception)
      编译期必须处理的异常(不处理会编译报错),如IOExceptionSQLException
    2. 非受检异常(Unchecked Exception)
      编译期不强制处理的异常,继承自RuntimeException,如NullPointerExceptionArrayIndexOutOfBoundsException
  1. 示例对比
// 处理Exception(程序应处理的情况)
try {
    FileReader fr = new FileReader("test.txt"); // 可能抛出IOException(受检异常)
} catch (IOException e) {
    // 处理异常:如提示文件不存在,程序可继续运行
    System.out.println("文件操作失败:" + e.getMessage());
}

// Error(程序无法处理的情况)
public class Test {
    public static void main(String[] args) {
        recursion(); // 无限递归会导致StackOverflowError
    }
    
    private static void recursion() {
        recursion(); // 递归调用,最终触发Error
    }
}
// 运行结果:抛出StackOverflowError,程序终止,无法通过try-catch恢复

总结:

对比维度ErrorException
性质系统级严重错误,JVM层面问题程序逻辑或外部环境异常,应用层面问题
可处理性不可处理(无需捕获)可处理(应捕获或声明抛出)
影响范围通常导致程序终止处理后程序可继续执行
典型例子OutOfMemoryErrorStackOverflowErrorNullPointerExceptionIOException

简单来说:Error是“致命故障”,程序无力回天;Exception是“可恢复的错误”,程序应该通过异常处理机制应对。

六、常见异常有哪些?

在Java中,常见的异常可分为非受检异常(Unchecked Exception,继承自RuntimeException受检异常(Checked Exception,直接继承自Exception 两大类,以下是开发中最常遇到的异常及其场景:

一、非受检异常(编译期不强制处理,运行时可能抛出)

  1. NullPointerException(空指针异常)

    • 场景:调用了null对象的方法或访问其属性。
    • 示例:
      String str = null;
      str.length(); // 抛出NullPointerException
      
  2. IndexOutOfBoundsException(索引越界异常)

    • 子类:ArrayIndexOutOfBoundsException(数组索引越界)、StringIndexOutOfBoundsException(字符串索引越界)。
    • 场景:访问数组、字符串时,索引值超出有效范围(如>=长度或<0)。
    • 示例:
      int[] arr = new int[3];
      arr[3] = 1; // 抛出ArrayIndexOutOfBoundsException(索引最大为2)
      
  3. ClassCastException(类型转换异常)

    • 场景:将对象强制转换为不兼容的类型。
    • 示例:
      Object obj = "hello";
      Integer num = (Integer) obj; // 抛出ClassCastException(String不能转为Integer)
      
  4. IllegalArgumentException(非法参数异常)

    • 场景:方法接收到不符合要求的参数(通常由开发者主动抛出)。
    • 示例:
      public void setAge(int age) {
          if (age < 0) {
              throw new IllegalArgumentException("年龄不能为负数");
          }
      }
      
  5. ArithmeticException(算术异常)

    • 场景:数学运算错误(最常见为除以0)。
    • 示例:
      int result = 10 / 0; // 抛出ArithmeticException
      
  6. NoSuchElementException(无此元素异常)

    • 场景:访问集合中不存在的元素(如迭代器遍历完后继续调用next())。
    • 示例:
      List<String> list = new ArrayList<>();
      Iterator<String> it = list.iterator();
      it.next(); // 抛出NoSuchElementException(集合为空)
      

二、受检异常(编译期必须处理,否则报错)

  1. IOException(输入输出异常)

    • 子类:FileNotFoundException(文件未找到)、EOFException(文件结束异常)等。
    • 场景:文件读写、网络传输等IO操作失败时抛出。
    • 示例:
      // 编译报错,必须用try-catch或throws处理
      FileReader fr = new FileReader("不存在的文件.txt");
      
  2. SQLException(数据库访问异常)

    • 场景:数据库连接、查询、更新等操作失败(如SQL语法错误、连接超时)。
  3. ClassNotFoundException(类未找到异常)

    • 场景:通过类名动态加载类时,找不到对应的.class文件。
    • 示例:
      Class.forName("com.mysql.jdbc.Driver"); // 若驱动类不存在则抛出
      
  4. InterruptedException(中断异常)

    • 场景:线程在睡眠(sleep())或等待(wait())时被中断。
    • 示例:
      try {
          Thread.sleep(1000);
      } catch (InterruptedException e) { // 必须处理
          e.printStackTrace();
      }
      
  5. ParseException(解析异常)

    • 场景:字符串解析为其他类型失败(如日期格式不匹配)。
    • 示例:
      SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
      sdf.parse("2023/13/01"); // 月份13无效,抛出ParseException
      

总结:

  • 非受检异常多由代码逻辑错误导致(如空指针、索引越界),需通过规范编码避免,通常不强制捕获。
  • 受检异常多与外部环境相关(如文件、网络、数据库),编译期强制处理,确保程序对外部异常情况有应对逻辑。

七、什么是反射?

在Java中,反射(Reflection) 是指程序在运行时可以动态获取类的信息(如类的属性、方法、构造器等),并能动态调用类的方法、访问或修改属性的机制。
简单来说,反射允许程序“看透”类的内部结构,并在运行时操作这些结构,而不需要在编译期就知道类的具体信息。

反射的核心作用:

  1. 动态获取类信息:运行时获取类的名称、父类、接口、属性、方法、注解等元数据。
  2. 动态创建对象:无需在编译期知道类名,运行时通过类名创建对象实例。
  3. 动态调用方法:运行时调用任意类的任意方法(包括私有方法)。
  4. 动态访问属性:运行时读写任意类的任意属性(包括私有属性)。

反射的实现基础:
反射机制的核心是基于Java的java.lang.reflect包,主要类包括:

  • Class:代表类的字节码对象,是反射的入口(获取类的所有信息都需要通过Class对象)。
  • Constructor:代表类的构造器,用于创建对象。
  • Method:代表类的方法,用于调用方法。
  • Field:代表类的属性,用于访问或修改属性值。

反射的简单示例:
假设我们有一个普通类User

public class User {
    private String name;
    private int age;

    public User() {}

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

    public void sayHello() {
        System.out.println("Hello, " + name);
    }

    private void setAge(int age) {
        this.age = age;
    }
}

通过反射操作User类:

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectionDemo {
    public static void main(String[] args) throws Exception {
        // 1. 获取Class对象(反射的入口)
        Class<?> userClass = Class.forName("User"); // 通过类名获取(运行时动态获取)
        // 也可以通过 User.class 或 new User().getClass() 获取

        // 2. 动态创建对象(调用有参构造器)
        Constructor<?> constructor = userClass.getConstructor(String.class, int.class);
        Object user = constructor.newInstance("张三", 20); // 等价于 new User("张三", 20)

        // 3. 动态调用公有方法
        Method sayHelloMethod = userClass.getMethod("sayHello");
        sayHelloMethod.invoke(user); // 等价于 user.sayHello(),输出:Hello, 张三

        // 4. 动态访问私有属性
        Field nameField = userClass.getDeclaredField("name");
        nameField.setAccessible(true); // 突破私有访问限制
        String name = (String) nameField.get(user);
        System.out.println("name属性值:" + name); // 输出:张三

        // 5. 动态调用私有方法
        Method setAgeMethod = userClass.getDeclaredMethod("setAge", int.class);
        setAgeMethod.setAccessible(true); // 突破私有访问限制
        setAgeMethod.invoke(user, 25); // 等价于 user.setAge(25)

        // 验证私有方法调用结果
        Field ageField = userClass.getDeclaredField("age");
        ageField.setAccessible(true);
        System.out.println("修改后的age:" + ageField.get(user)); // 输出:25
    }
}

反射的应用场景:

  1. 框架开发:Spring、MyBatis等框架的核心机制依赖反射。例如,Spring的IOC容器通过反射创建对象并注入依赖;MyBatis通过反射将数据库查询结果映射为Java对象。
  2. 动态代理:AOP(面向切面编程)的实现基础,通过反射在方法执行前后插入增强逻辑。
  3. 序列化与反序列化:将对象转为字节流(如JSON序列化)或从字节流恢复对象时,需要通过反射操作对象的属性。
  4. 工具类开发:如日志框架、ORM框架等,需要动态处理任意类的属性和方法。

反射的优缺点:

  • 优点:灵活性高,允许程序在运行时动态操作类,降低代码耦合度,是很多框架的基础。
  • 缺点
    • 性能开销:反射操作需要解析字节码,比直接调用(编译期确定)慢。
    • 破坏封装性:可以访问私有成员,可能违反类的设计初衷。
    • 代码可读性降低:反射代码相对复杂,不如直接调用直观。

总结:
反射是Java中一种强大的动态编程机制,允许程序在运行时“洞察”和操作类的内部结构。它是许多框架和中间件的核心,但也存在性能和封装性方面的权衡,实际开发中应根据场景合理使用。

八、为什么引入反射?

Java引入反射机制的核心目的是增强程序的灵活性和动态性,打破编译期的类型约束,让程序能够在运行时动态处理未知的类和对象。这种机制解决了传统静态编程(编译期确定所有操作)的局限性,为框架开发、动态适配等场景提供了底层支持。

具体来说,引入反射的核心价值体现在以下几个方面:

  1. 解决“编译期未知类型”的问题
    传统编程中,使用一个类必须在编译期明确知道类名,并通过new关键字创建对象(如User user = new User())。但在很多场景下,程序在编译期无法确定要操作的类:

    • 例如,Spring框架在启动时需要根据配置文件(如applicationContext.xml)中的类名(字符串形式,如"com.example.User")创建对象。
    • 此时只能通过反射(Class.forName("com.example.User").newInstance())在运行时动态加载类并创建实例,而无法在编译期用new关键字硬编码。
  2. 降低代码耦合度,支持通用框架开发
    反射允许开发者编写通用代码,无需针对特定类定制逻辑。例如:

    • MyBatis框架需要将数据库查询结果映射为任意Java对象(可能是UserOrderProduct等),它通过反射动态获取对象的属性(如userIduserName),并调用setter方法赋值。
    • 如果没有反射,框架必须为每个可能的类编写单独的映射代码,这显然不可行。
    • 反射让框架能够“通用化”处理所有类,极大降低了代码耦合。
  3. 支持动态代理和AOP编程
    反射是动态代理的基础,而动态代理是AOP(面向切面编程)的核心实现方式:

    • 例如,Spring AOP需要在目标方法执行前后插入日志、事务等增强逻辑。通过反射,代理类可以在运行时动态调用目标方法(即使编译期不知道目标方法的具体信息)。
    • 没有反射,就无法实现“在不修改原有代码的情况下动态增强方法功能”。
  4. 允许访问类的私有成员,满足特殊场景需求
    Java的封装机制限制了对私有属性和方法的直接访问,但某些场景下需要突破这种限制:

    • 例如,序列化框架(如Jackson)在将对象转为JSON时,需要读取对象的私有属性(如private String name)。
    • 反射通过setAccessible(true)可以临时取消访问检查,读取/修改私有成员,满足这类特殊需求。
  5. 支持动态加载类和热部署
    反射配合类加载器,可以实现类的动态加载和替换(热部署):

    • 例如,某些中间件需要在程序运行时加载新的插件类(编译期不存在这些类),通过反射可以动态加载.class文件并创建实例。
    • 这在模块化开发、插件化系统中至关重要。

一句话总结:
反射机制让Java从“静态语言”具备了一定的“动态语言”特性,允许程序在运行时根据实际情况调整行为。它是框架、中间件、工具类的“基础设施”,没有反射,Spring、MyBatis、Jackson等几乎所有主流Java框架都无法实现。

当然,反射也有性能开销和破坏封装性的缺点,因此通常用于框架底层,而非业务逻辑代码中。

九、什么是泛型 ?

在Java中,泛型(Generics) 是一种在编译期提供类型约束检查的机制,允许类、接口、方法在定义时声明类型参数,并在使用时指定具体类型。简单来说,泛型让代码可以“参数化类型”,从而实现代码复用类型安全

泛型的核心作用:

  1. 类型安全:编译期检查数据类型,避免运行时出现ClassCastException(类型转换异常)。
  2. 代码复用:一套逻辑可以适配多种数据类型,无需为每种类型重复编写代码。
  3. 可读性提升:通过类型参数明确代码操作的数据类型,增强代码的可读性和可维护性。

泛型的基本用法
1. 泛型类(Generic Class)
在类定义时声明类型参数,使类可以操作多种类型的数据。

// 定义泛型类,T为类型参数(通常用大写字母表示,如T、E、K、V)
public class Box<T> {
    private T content; // 使用类型参数T定义属性

    // 使用类型参数T定义方法
    public void setContent(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }
}

// 使用泛型类:指定具体类型(如String、Integer)
public class Main {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        stringBox.setContent("Hello"); // 只能传入String类型
        String str = stringBox.getContent(); // 无需强制类型转换

        Box<Integer> intBox = new Box<>();
        intBox.setContent(123); // 只能传入Integer类型
        Integer num = intBox.getContent();
    }
}

2. 泛型方法(Generic Method)
在方法声明时独立声明类型参数,使方法可以适配多种类型的参数和返回值。

public class GenericMethodDemo {
    // 定义泛型方法,T为类型参数
    public static <T> T getFirstElement(T[] array) {
        if (array != null && array.length > 0) {
            return array[0];
        }
        return null;
    }

    public static void main(String[] args) {
        String[] strArray = {"A", "B", "C"};
        String firstStr = getFirstElement(strArray); // 自动适配String类型

        Integer[] intArray = {1, 2, 3};
        Integer firstInt = getFirstElement(intArray); // 自动适配Integer类型
    }
}

3. 泛型接口(Generic Interface)
在接口定义时声明类型参数,实现类需指定具体类型或继续保留泛型。

// 定义泛型接口
public interface Generator<T> {
    T generate();
}

// 实现接口时指定具体类型(如String)
public class StringGenerator implements Generator<String> {
    @Override
    public String generate() {
        return "Generated string";
    }
}

// 实现接口时保留泛型
public class NumberGenerator<T extends Number> implements Generator<T> {
    @Override
    public T generate() {
        // 实现逻辑...
        return null;
    }
}

泛型的关键特性:

  • 类型擦除(Type Erasure):Java泛型是“编译期语法糖”,编译后会擦除类型参数信息,字节码中不保留泛型类型(如Box<String>会被擦除为Box)。这是为了兼容泛型出现前的旧代码。
  • 通配符(Wildcards):使用?表示未知类型,配合extends(上限)和super(下限)限制范围,例如:
    // 只能接收Number及其子类(如Integer、Double)
    public void printNumbers(List<? extends Number> numbers) { ... }
    
    // 只能接收Integer及其父类(如Number、Object)
    public void addIntegers(List<? super Integer> list) { ... }
    
  • 类型边界(Bounded Type Parameters):限制类型参数的范围,例如class Box<T extends Number>表示T必须是Number的子类。

为什么需要泛型?
没有泛型时,需使用Object类型适配多种数据,存在两个问题:

  1. 类型不安全:可以向集合中添加任意类型数据,运行时可能出现类型转换异常。
    List list = new ArrayList();
    list.add("hello");
    list.add(123); // 编译不报错,但逻辑上错误
    String str = (String) list.get(1); // 运行时抛出ClassCastException
    
  2. 冗余的类型转换:从集合中获取元素时必须强制类型转换,代码繁琐。

泛型通过编译期类型检查解决了这些问题,是Java集合框架(如ListMap)的基础,也是编写通用、安全代码的重要工具。

十、泛型的优点?

泛型(Generics)是Java中一项重要的特性,其核心价值在于提升代码的安全性、复用性和可读性,具体优点如下:

  1. 编译期类型安全,避免运行时异常
  • 核心作用:泛型允许在编译期指定集合或类操作的数据类型,编译器会自动检查类型匹配,阻止不兼容类型的数据存入。
  • 解决的问题:没有泛型时,使用Object类型存储数据会导致“编译期不报错,运行时抛ClassCastException”的隐患。
  • 示例对比
    // 无泛型:存在类型安全问题
    List list = new ArrayList();
    list.add("字符串");
    list.add(123); // 编译不报错(逻辑错误未被发现)
    String str = (String) list.get(1); // 运行时抛ClassCastException
    
    // 有泛型:编译期检查
    List<String> strList = new ArrayList<>();
    strList.add("字符串");
    strList.add(123); // 编译直接报错(阻止错误存入)
    String str2 = strList.get(0); // 无需强制转换,无异常风险
    
  1. 减少冗余的类型转换,简化代码
  • 泛型会自动推断数据类型,从集合或泛型类中获取数据时无需手动强制转换,减少代码冗余并避免转换错误。
  • 例如:使用List<String>时,get()方法直接返回String,无需(String) list.get(0)这样的转换。
  1. 提高代码复用性,实现通用逻辑
  • 泛型允许一套代码逻辑适配多种数据类型,无需为每种类型重复编写相似代码。
  • 示例:一个泛型工具类可同时处理StringIntegerUser等任意类型:
    // 泛型工具类:打印任意类型数组的元素
    public class ArrayUtil<T> {
        public void printArray(T[] array) {
            for (T element : array) {
                System.out.println(element);
            }
        }
    }
    
    // 使用时适配不同类型
    String[] strs = {"A", "B"};
    Integer[] nums = {1, 2};
    new ArrayUtil<String>().printArray(strs); // 处理字符串数组
    new ArrayUtil<Integer>().printArray(nums); // 处理整数数组
    
  1. 明确代码意图,提升可读性和可维护性
  • 泛型通过类型参数(如List<User>Map<String, Integer>)直观地表明代码操作的数据类型,让其他开发者能快速理解代码逻辑。
  • 例如:Map<String, User>一眼就能看出“键是字符串,值是User对象”,比原始的Map(默认Object类型)更清晰。
  1. 支持泛型集合和框架的实现
  • Java集合框架(ListMapSet等)完全依赖泛型实现,没有泛型就无法保证集合操作的类型安全。
  • 主流框架(如Spring、MyBatis)也大量使用泛型实现通用功能(如MyBatis的Mapper<T>接口,可适配任意实体类)。

总结:
泛型的核心优点可概括为:在编译期保障类型安全,减少类型转换,提升代码复用性和可读性。它是现代Java开发中不可或缺的特性,尤其在集合操作、工具类开发和框架设计中发挥着关键作用,让代码更健壮、更简洁、更易于维护。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值