MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java中的非泛型类与泛型类的对比

2021-03-207.3k 阅读

Java中的非泛型类与泛型类的对比

非泛型类基础

在Java早期版本中,开发者创建的类通常是非泛型类。非泛型类在定义时,其成员变量、方法参数和返回值的类型都是明确指定的。例如,我们创建一个简单的Box类来存储整数:

public class Box {
    private int value;

    public Box(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

在上述代码中,Box类只能存储int类型的数据。如果我们想要存储其他类型的数据,比如String,就需要重新创建一个新的类,如下:

public class StringBox {
    private String value;

    public StringBox(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

从这两个例子可以看出,非泛型类对于不同数据类型的处理缺乏灵活性,导致代码的重复度较高。每个新的数据类型都需要创建一个新的类,这增加了代码的维护成本。

泛型类的引入

为了解决非泛型类的局限性,Java 5.0引入了泛型。泛型允许我们在类、接口或方法的定义中使用类型参数。使用泛型,我们可以创建一个通用的Box类,能够存储任何类型的数据。下面是一个简单的泛型Box类的示例:

public class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

在上述代码中,<T>是类型参数,它可以代表任何数据类型。当我们使用这个Box类时,可以在创建对象时指定具体的类型。例如:

Box<Integer> integerBox = new Box<>(10);
Box<String> stringBox = new Box<>("Hello, World!");

通过这种方式,我们可以使用同一个Box类来存储不同类型的数据,大大提高了代码的复用性。

类型安全性

非泛型类的类型安全问题

非泛型类在处理数据类型时存在类型安全隐患。以一个简单的容器类ObjectContainer为例,它可以存储任何类型的对象:

public class ObjectContainer {
    private Object value;

    public ObjectContainer(Object value) {
        this.value = value;
    }

    public Object getValue() {
        return value;
    }

    public void setValue(Object value) {
        this.value = value;
    }
}

当我们使用这个类时,可能会出现类型转换错误。例如:

ObjectContainer container = new ObjectContainer(10);
String str = (String) container.getValue();

在上述代码中,我们将一个Integer类型的对象放入ObjectContainer中,然后试图将其转换为String类型,这会导致运行时的ClassCastException异常。

泛型类的类型安全优势

泛型类在编译时就会进行类型检查,从而避免了运行时的类型转换错误。继续以泛型Box类为例:

Box<Integer> integerBox = new Box<>(10);
// 以下代码会在编译时出错
// Box<Integer> wrongBox = new Box<>("Hello");

在上述代码中,如果我们试图将一个String类型的对象放入Box<Integer>中,编译器会报错,从而保证了类型的安全性。

代码可读性和可维护性

非泛型类对代码可读性的影响

非泛型类在处理多种数据类型时,代码的可读性会受到影响。例如,我们有一个方法用于打印容器中的值:

public class NonGenericPrinter {
    public void printValue(ObjectContainer container) {
        Object value = container.getValue();
        if (value instanceof Integer) {
            System.out.println("Integer value: " + (Integer) value);
        } else if (value instanceof String) {
            System.out.println("String value: " + (String) value);
        }
    }
}

在上述代码中,printValue方法需要通过instanceof关键字来判断对象的实际类型,这使得代码变得冗长且难以理解。

泛型类对代码可读性和可维护性的提升

泛型类可以使代码更加清晰易懂。以下是使用泛型Box类的打印方法:

public class GenericPrinter {
    public <T> void printValue(Box<T> box) {
        System.out.println("Value: " + box.getValue());
    }
}

在上述代码中,printValue方法使用了泛型类型参数<T>,不需要进行类型判断,代码更加简洁明了。同时,由于泛型的类型安全性,维护代码时也不用担心类型转换错误,提高了代码的可维护性。

性能方面的差异

非泛型类的性能特点

非泛型类在处理基本数据类型时,由于其类型是明确的,没有额外的类型检查开销,性能相对较高。例如,前面提到的Box类存储int类型数据时,其操作直接针对int类型,没有装箱和拆箱的过程。但是,当非泛型类存储对象类型时,由于需要使用Object类型来通用存储,可能会导致运行时的类型转换开销。

泛型类的性能特点

泛型类在编译时进行类型检查,虽然在编译阶段会增加一些时间开销,但在运行时由于类型安全得到保障,不会出现类型转换错误导致的额外开销。然而,当泛型类用于存储基本数据类型时,会发生装箱和拆箱操作。例如:

Box<Integer> integerBox = new Box<>(10);
int num = integerBox.getValue();

在上述代码中,10被装箱成Integer对象放入Box中,然后又被拆箱成int类型。装箱和拆箱操作会带来一定的性能损耗。不过,在现代的Java虚拟机中,对于频繁的装箱和拆箱操作,会进行一些优化,在大多数情况下这种性能损耗并不显著。

继承和多态在非泛型类与泛型类中的表现

非泛型类的继承和多态

在非泛型类中,继承和多态的实现与普通的Java类继承规则一致。例如,我们有一个Animal类和它的子类Dog

class Animal {
    public void makeSound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }
}

我们可以创建一个Animal类型的容器来存储Dog对象,利用多态特性调用makeSound方法:

ObjectContainer animalContainer = new ObjectContainer(new Dog());
Animal animal = (Animal) animalContainer.getValue();
animal.makeSound();

在上述代码中,虽然存在类型转换,但利用了Java的继承和多态机制,实现了对不同类型对象的统一处理。

泛型类的继承和多态

泛型类在继承和多态方面有一些特殊的规则。假设我们有一个泛型类GenericClass<T>,以及它的子类SubGenericClass<T>

class GenericClass<T> {
    public void printType(T value) {
        System.out.println("Type: " + value.getClass().getName());
    }
}

class SubGenericClass<T> extends GenericClass<T> {
    @Override
    public void printType(T value) {
        System.out.println("Sub - Type: " + value.getClass().getName());
    }
}

当我们使用泛型类的继承和多态时,需要注意类型的一致性。例如:

GenericClass<String> genericString = new SubGenericClass<>();
genericString.printType("Hello");

在上述代码中,SubGenericClass的实例可以赋值给GenericClass类型的变量,这遵循了Java的继承规则。但是,泛型类在继承时还存在一些限制,比如不能使用泛型类型参数来创建数组。例如:

// 以下代码会编译错误
T[] array = new T[10];

这是因为在运行时,Java的泛型类型信息会被擦除,无法确定实际的数组类型。

通配符在泛型类中的应用及与非泛型类的区别

通配符在泛型类中的作用

在泛型类中,通配符用于解决类型之间的兼容性问题。通配符主要有两种形式:?(无界通配符)和? extends Type(上界通配符)、? super Type(下界通配符)。

无界通配符?表示可以是任何类型。例如,我们有一个方法用于打印Box中的值:

public void printBox(Box<?> box) {
    System.out.println(box.getValue());
}

上述方法可以接受任何类型的Box对象。

上界通配符? extends Type表示类型必须是TypeType的子类。例如:

class Fruit {}
class Apple extends Fruit {}

public void printFruitBox(Box<? extends Fruit> box) {
    Fruit fruit = box.getValue();
    System.out.println(fruit);
}

在上述代码中,printFruitBox方法只能接受存储Fruit或其子类(如Apple)的Box对象。

下界通配符? super Type表示类型必须是TypeType的超类。例如:

public void addAppleToBox(Box<? super Apple> box) {
    box.setValue(new Apple());
}

在上述代码中,addAppleToBox方法可以接受存储Apple或其超类(如ObjectFruit)的Box对象。

通配符与非泛型类的区别

非泛型类不存在通配符的概念。在非泛型类中,类型是固定的,不存在类型兼容性的灵活处理。例如,在前面的ObjectContainer类中,它只能通过Object类型来通用存储对象,无法像泛型类使用通配符那样在类型安全的前提下实现更灵活的类型处理。通配符使得泛型类在处理不同类型关系时更加灵活,同时又保证了类型安全性,而非泛型类则缺乏这种能力。

反射与非泛型类和泛型类的交互

反射与非泛型类

反射是Java提供的一种在运行时获取类的信息并操作类的属性和方法的机制。对于非泛型类,反射的使用相对简单直接。例如,我们可以通过反射获取Box类(前面定义的存储int类型的非泛型类)的构造函数并创建对象:

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class NonGenericReflection {
    public static void main(String[] args) {
        try {
            Class<?> boxClass = Class.forName("Box");
            Constructor<?> constructor = boxClass.getConstructor(int.class);
            Box box = (Box) constructor.newInstance(10);
            int value = box.getValue();
            System.out.println("Value: " + value);
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,我们通过反射获取Box类的构造函数,并使用构造函数创建对象,然后调用其getValue方法。

反射与泛型类

反射与泛型类的交互稍微复杂一些。由于Java泛型的类型擦除机制,在运行时泛型类型信息会丢失。例如,对于泛型Box类:

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

public class GenericReflection {
    public static void main(String[] args) {
        try {
            Class<?> boxClass = Class.forName("Box");
            Constructor<?> constructor = boxClass.getConstructor(Object.class);
            Box<String> box = (Box<String>) constructor.newInstance("Hello");
            Field valueField = boxClass.getDeclaredField("value");
            valueField.setAccessible(true);
            Object value = valueField.get(box);
            System.out.println("Value: " + value);
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,虽然我们创建了一个Box<String>对象,但在反射获取构造函数和字段时,泛型类型信息已经丢失,只能将其当作普通的类来处理。不过,Java也提供了一些方法来获取泛型类型信息,例如通过ParameterizedType接口。例如,如果我们有一个泛型方法:

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public class GenericMethodReflection {
    public <T> void genericMethod(Box<T> box) {}

    public static void main(String[] args) {
        try {
            Method method = GenericMethodReflection.class.getMethod("genericMethod", Box.class);
            Type[] genericParameterTypes = method.getGenericParameterTypes();
            if (genericParameterTypes[0] instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) genericParameterTypes[0];
                Type typeArgument = parameterizedType.getActualTypeArguments()[0];
                System.out.println("Type argument: " + typeArgument.getTypeName());
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,通过ParameterizedType接口,我们可以获取泛型方法参数的实际类型参数。这表明虽然Java泛型存在类型擦除,但在某些情况下仍然可以在运行时获取泛型类型信息。

序列化与非泛型类和泛型类

非泛型类的序列化

在Java中,实现Serializable接口的类可以被序列化。对于非泛型类,序列化过程相对直接。例如,我们修改前面的Box类(存储int类型的非泛型类)使其可序列化:

import java.io.*;

public class Box implements Serializable {
    private int value;

    public Box(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public static void main(String[] args) {
        Box box = new Box(10);
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("box.ser"))) {
            oos.writeObject(box);
        } catch (IOException e) {
            e.printStackTrace();
        }

        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("box.ser"))) {
            Box deserializedBox = (Box) ois.readObject();
            System.out.println("Deserialized value: " + deserializedBox.getValue());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,Box类实现了Serializable接口,通过ObjectOutputStreamObjectInputStream实现了对象的序列化和反序列化。

泛型类的序列化

泛型类的序列化也需要实现Serializable接口。例如,我们的泛型Box类:

import java.io.*;

public class Box<T> implements Serializable {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }

    public static void main(String[] args) {
        Box<String> box = new Box<>("Hello");
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("box.ser"))) {
            oos.writeObject(box);
        } catch (IOException e) {
            e.printStackTrace();
        }

        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("box.ser"))) {
            Box<String> deserializedBox = (Box<String>) ois.readObject();
            System.out.println("Deserialized value: " + deserializedBox.getValue());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,泛型Box类同样实现了Serializable接口进行序列化和反序列化。需要注意的是,由于Java泛型的类型擦除,在反序列化时,泛型类型信息不会被保留。例如,如果我们在反序列化后将Box<String>对象赋值给Box<Integer>类型的变量,编译器不会报错,但运行时可能会出现类型转换错误。

总结

通过以上对Java中非泛型类和泛型类在多个方面的对比,我们可以清晰地看到它们各自的特点和适用场景。非泛型类在处理简单、固定类型的数据时,具有性能优势和简单直接的特点,但在处理多种数据类型时缺乏灵活性,代码复用性低,且存在类型安全隐患。而泛型类则通过类型参数的引入,大大提高了代码的复用性和类型安全性,增强了代码的可读性和可维护性。虽然泛型类在处理基本数据类型时会有装箱和拆箱的性能损耗,且在继承、反射和序列化等方面有一些特殊的规则和处理方式,但在大多数现代Java开发场景中,泛型类的优势使其成为更常用的选择。开发者在实际编程中,应根据具体需求,合理选择使用非泛型类或泛型类,以达到最佳的编程效果。