侧边栏壁纸
博主头像
极客日记 博主等级

行动起来,活在当下

  • 累计撰写 93 篇文章
  • 累计创建 17 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

Java 基础 - 泛型

Jack.Jia
2022-09-19 / 0 评论 / 0 点赞 / 6 阅读 / 0 字

前言

为什么要学习 Java?

  1. 因为接手了慧讯网 app 项目,其中有一些 cordova 插件是需要安卓(Java)开发的,所以就想先学习一下 Java 基础,然后再学习安卓相关的知识,后续可以自己做一些插件的开发。
  2. Java 作为热度最高的语言之一,在我个人的角度上还是想去学习了解一下的。

为什么要分享泛型这块的内容?

  1. 学习的时候发现这部分内容比较不好理解,所以就想再整理一下,分享出来,查漏补全。
  2. 感觉泛型的这个概念在弱类型语言中没有,所以想和大家探讨一下。
  3. 篇幅时间有限,还有一些没有分享,例如泛型继承、上下边界等等。

Java 简介

Java 最早是由 SUN 公司(已被 Oracle 收购)的詹姆斯·高斯林(高司令,人称 Java 之父)在上个世纪 90 年代初开发的一种编程语言,最初被命名为 Oak,目标是针对小型家电设备的嵌入式应用,结果市场没啥反响。谁料到互联网的崛起,让 Oak 重新焕发了生机,于是 SUN 公司改造了 Oak,在 1995 年以 Java 的名称正式发布,原因是 Oak 已经被人注册了,因此 SUN 注册了 Java 这个商标。随着互联网的高速发展,Java 逐渐成为最重要的网络编程语言。

Java 介于编译型语言和解释型语言之间。编译型语言如 C、C++,代码是直接编译成机器码执行,但是不同的平台(x86、ARM 等)CPU 的指令集不同,因此,需要编译出每一种平台的对应机器码。解释型语言如 Python、Ruby 没有这个问题,可以由解释器直接加载源码然后运行,代价是运行效率太低。而 Java 是将代码编译成一种“字节码”,它类似于抽象的 CPU 指令,然后,针对不同平台编写虚拟机,不同平台的虚拟机负责加载字节码并执行,这样就实现了“一次编写,到处运行”的效果。当然,这是针对 Java 开发者而言。对于虚拟机,需要为每个平台分别开发。为了保证不同平台、不同公司开发的虚拟机都能正确执行 Java 字节码,SUN 公司制定了一系列的 Java 虚拟机规范。从实践的角度看,JVM 的兼容性做得非常好,低版本的 Java 字节码完全可以正常运行在高版本的 JVM 上。

类 (class) && 方法 (method)

面向对象编程,是一种通过对象的方式,把现实世界映射到计算机模型的一种编程方法。

现实世界中,我们定义了“人”这种抽象概念,而具体的人则是“小明”、“小红”、“小军”等一个个具体的人。所以,“人”可以定义为一个类(class),而具体的人则是实例(instance)。

在 Java 中,创建一个类,例如,给这个类命名为 Person,就是定义一个 class

class Person {
    public String name;
    public int age;
}

一个 class 可以包含多个字段(field),字段用来描述一个类的特征。上面的 Person 类,我们定义了两个字段,一个是 String 类型的字段,命名为 name,一个是 int 类型的字段,命名为 age。因此,通过 class,把一组数据汇集到一个对象上,实现了数据封装。

public 是用来修饰字段的,它表示这个字段可以被外部访问。

一个 class 可以包含多个 field,例如,我们给 Person 类就定义了两个 field

class Person {
    public String name;
    public int age;
}

但是,直接把 fieldpublic 暴露给外部可能会破坏封装性。比如,代码可以这样写:

Person ming = new Person();
ming.name = "Xiao Ming";
ming.age = -99; // age设置为负数 

所以我们需要使用方法(method)来让外部代码可以间接修改 field

package com.learning.person;

public class PersonMain {
    public static void main(String[] args) {
        Person ming = new Person();
        ming.setName("Xiao Ming"); // 设置name
        ming.setAge(12); // 设置age
        System.out.println(ming.getName() + ", " + ming.getAge()); // Xiao Ming, 12
    }
}

class Person {
    private String name;
    private int age;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        if (age < 0 || age > 100) {
            throw new IllegalArgumentException("invalid age value");
        }
        this.age = age;
    }
}

总结:

  1. 定义 class 就是定义了一种数据类型,对应的 instance 是这种数据类型的实例;
  2. class 定义的 field,在每个 instance 都会拥有各自的 field,且互不干扰;
  3. 方法可以让外部代码安全地访问实例字段;
  4. 方法是一组执行语句,并且可以执行任意逻辑;

抽象方法 && 抽象类

如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法:

class Person {
    public abstract void run();
}

把一个方法声明为 abstract,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person 类也无法被实例化。编译器会告诉我们,无法编译 Person 类,因为它包含抽象方法。

必须把 Person 类本身也声明为 abstract,才能正确编译它:

abstract class Person {
    public abstract void run();
}

如果一个 class 定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用 abstract 修饰。

因为无法执行抽象方法,因此这个类也必须申明为抽象类(abstract class)。

使用 abstract 修饰的类就是抽象类。我们无法实例化一个抽象类:

Person p = new Person(); // 编译错误

无法实例化的抽象类有什么用?

因为抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。因此,抽象方法实际上相当于定义了“规范”。

例如,Person 类定义了抽象方法 run(),那么,在实现子类 Student 的时候,就必须覆写 run() 方法:

package com.learning.abstract_;

public class PersonMain {
    public static void main(String[] args) {
        Person p = new Student();
        p.run(); // Student.run
    }
}

abstract class Person {
    public abstract void run();
}

class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}

总结:

  1. 通过 abstract 定义的方法是抽象方法,它只有定义,没有实现。抽象方法定义了子类必须实现的接口规范;
  2. 定义了抽象方法的 class 必须被定义为抽象类,从抽象类继承的子类必须实现抽象方法;
  3. 面向抽象编程使得调用者只关心抽象方法的定义,不关心子类的具体实现。

接口 interface

在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。

如果一个抽象类没有字段,所有方法全部都是抽象方法:

abstract class Person {
    public abstract void run();
    public abstract String getName();
}

就可以把该抽象类改写为接口:interface

在 Java 中,使用 interface 可以声明一个接口:

interface Person {
    void run();
    String getName();
}

所谓 interface,就是比抽象类还要抽象的纯抽象接口,因为它连 field(实例字段) 都不能有。因为接口定义的所有方法默认都是 public abstract 的,所以这两个修饰符不需要写出来(写不写效果都一样)。

接口其实可以定义 field,但是这个 field 必须是一个 public static final 的常量, 这个时候 field 其实是一个类常量了

当一个具体的 class 去实现一个 interface 时,需要使用 implements 关键字。举个例子:

class Student implements Person {
    private String name;

    public Student(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(this.name + " run");
    }

    @Override
    public String getName() {
        return this.name;
    }
}

我们知道,在 Java 中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个 interface,例如:

class Student implements Person, Hello { // 实现了两个interface
    ...
}

总结:

  1. Java 的接口(interface)定义了纯抽象规范,一个类可以实现多个接口;
  2. 接口的所有方法都是抽象方法,接口不能定义实例字段;

泛型

泛型这部分深入讲的话篇幅有点多,所以先只讲一下基础的。

概述

泛型在 java 中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。

什么是泛型?为什么要使用泛型?

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用 / 调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

举个栗子

ArrayList 其实就是泛型,如果不定义泛型类型时,泛型类型实际上就是 Object
Java 标准库中的 ArrayList<T> 实现了 List<T> 接口,它可以向上转型为 List<T>

看看下面这段代码:

package com.learning.flood;

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

public class Main {
    public static void main(String[] args) {
        List arrayList = new ArrayList(); // ArrayList 是Java标准库提供的类型,它可以看作“可变长度”的数组,因为用起来比数组更方便。
        arrayList.add("abc");
        arrayList.add(100);

        for(int i = 0; i< arrayList.size();i++){
            String item = (String)arrayList.get(i);
            System.out.println("泛型测试: item = " + item);
        }
    }
}

毫无疑问,肯定会报错

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String // 类型转换失败

ArrayList 可以存放任意类型,例子中添加了一个 String 类型,添加了一个 Integer 类型,再使用时都以 String 的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。

我们将第一行声明初始化 list 的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。

List<String> arrayList = new ArrayList<>();
...
//arrayList.add(100); 在编译阶段,编译器就会报错

编写泛型 && 擦拭法

泛型是一种类似”模板代码“的技术,不同语言的泛型实现方式不一定相同,Java 语言的泛型实现方式是擦拭法(Type Erasure)

所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。

例如,我们编写了一个泛型类 Generic<T>,这是编译器看到的代码:

package com.learning.flood.type_erasure;

public class GenericMain<T>{ // T 就是参数化的类型
    public static void main(String[] args) {
        Generic<String> g = new Generic<>("Hello world");
        String key = g.getKey();
        System.out.println(key);
    }
}

class Generic<T>{
    // key这个成员变量的类型为T, T的类型由外部指定
    private T key;

    public Generic(T key) { // 泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ // 泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}

而虚拟机根本不知道泛型。这是虚拟机执行的代码:

public class Generic {
    private Object key;

    public Generic(Object key) {
        this.key = key;
    }

    public Object getKey(){
        return key;
    }
}

因此,Java 使用擦拭法实现泛型,导致了:

  1. 编译器把类型 <T> 视为 Object
  2. 编译器根据 <T> 实现安全的强制转型。

使用泛型的时候,我们编写的代码也是编译器看到的代码:

Generic<String> g = new Generic<>("Hello world");
String key = g.getKey();
System.out.println(key); // Hello world

而虚拟机执行的代码并没有泛型:

Generic g = new Generic("Hello world");
String key = (String) g.getKey();

所以,Java 的泛型是由编译器在编译时实行的,编译器内部永远把所有类型 T 视为 Object 处理,但是,在需要转型的时候,编译器会根据 T 的类型自动为我们实行安全地强制转型。

可以用下面的代码验证上面说的内容:

package com.learning.flood.type_erasure;

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

public class Main {
    public static void main(String[] args) {
        List<String> stringArrayList = new ArrayList<String>();
        List<Integer> integerArrayList = new ArrayList<Integer>();

        Class classStringArrayList = stringArrayList.getClass();
        Class classIntegerArrayList = integerArrayList.getClass();

        System.out.println("classStringArrayList: " + classStringArrayList);
        System.out.println("classIntegerArrayList: " + classIntegerArrayList);

        if (classStringArrayList.equals(classIntegerArrayList)) {
            System.out.println("泛型测试: 类型相同");
        }
    }
}

// 输出结果
// classIntegerArrayList: class java.util.ArrayList
// classIntegerArrayList: class java.util.ArrayList
// 泛型测试: 类型相同

总结:

  1. Java 的泛型是采用擦拭法实现的
  2. 泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型

泛型的使用

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

一个最普通的泛型类:

package com.learning.flood.flood_class;

public class GenericMain {
    public static void main(String[] args) {
        /**
         * 泛型的类型参数只能是类类型(包括自定义类),不能是基本类型,例如:int、char
         * 泛型的类型参数只能是类类型(包括自定义类),不能是基本类型,例如:int、char
         */
        Generic<Integer> genericInteger = new Generic<Integer>(123456);

        // 传入的实参类型需与泛型的类型参数 类型相同,即为String.
        Generic<String> genericString = new Generic<String>("key_value");
        System.out.println("泛型测试: key is " + genericInteger.getKey());
        System.out.println("泛型测试: key is " + genericString.getKey());

        // 输出结果
        // 泛型测试: key is 123456
        // 泛型测试: key is key_value
    }
}

/**
 * 此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
 * 在实例化泛型类时,必须指定T的具体类型
 */
class Generic<T>{
    // key这个成员变量的类型为T, T的类型由外部指定
    private T key;

    public Generic(T key) { // 泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ // 泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}

定义的泛型类,就一定要传入泛型类型实参么?

并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

泛型接口

泛型接口与泛型类的定义及使用基本相同。可以看一个例子:

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

当实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override // 注解,表示覆写接口的next方法
    public T next() {
        return null;
    }
}

当实现泛型接口的类,传入泛型实参时:

/**
 * 传入泛型实参时:
 * 定义一个类实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
 * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
 */
class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

泛型方法

在 java 中, 泛型类的定义非常简单,但是泛型方法就比较复杂了。

尤其是我们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样非常容易将泛型方法理解错了。

区别:

  1. 泛型类,是在实例化类的时候指明泛型的具体类型;
  2. 泛型方法,是在调用方法的时候指明泛型的具体类型 。

泛型方法的基本介绍:

package com.learning.flood.flood_method;

import java.util.ArrayList;

public class genericMethodMain {

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        genericMethodClass g = new genericMethodClass();
        Object obj = g.genericMethod(Class.forName("java.util.ArrayList"));
        System.out.println(obj.toString()); // []
        System.out.println(obj.getClass()); // java.util.ArrayList
        ArrayList<String> array = new ArrayList<>();
        g.showClassName(array); // java.util.ArrayList
    }
}

class genericMethodClass {
    /**
     * @param tClass 传入的泛型实参
     * @return T 返回值为T类型
     * 说明:
     *  1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
     *  2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
     *  3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
     *  4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
     */
    public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
            IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
    }

    // 这不是一个泛型方法,这就是一个普通的方法,只是使用了ArrayList<String>这个泛型类做形参而已。
    public void showClassName(ArrayList<String> array){
        System.out.println("泛型测试: class name is " + array.getClass());
    }
}

类中的泛型方法

当泛型方法出现在泛型类中时,情况是非常特殊的,我们通过一个例子看一下

package com.learning.flood.flood_method;

public class GenericFruit {
    static class Fruit{
        @Override
        public String toString() {
            return "fruit";
        }
    }

    static class Apple extends Fruit{
        @Override
        public String toString() {
            return "apple";
        }
    }

    static class Person{
        @Override
        public String toString() {
            return "Person";
        }
    }

    static class GenerateTest<T>{
        public void show_1(T t){
            System.out.println(t.toString());
        }

        /**
         * 在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
         * 由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
         */
        public <E> void show_3(E t){
            System.out.println(t.toString());
        }

        // 在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
        public <T> void show_2(T t){
            System.out.println(t.toString());
        }
    }

    public static void main(String[] args) {
        Apple apple = new Apple();
        Person person = new Person();

        GenerateTest<Fruit> generateTest = new GenerateTest<>();
        // apple是Fruit的子类,所以这里可以
        generateTest.show_1(apple); // apple
        // 编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
        // generateTest.show_1(person);

        // 使用这两个方法都可以成功
        generateTest.show_2(apple); // apple
        generateTest.show_2(person); // Person

        //使用这两个方法也都可以成功
        generateTest.show_3(apple); // apple
        generateTest.show_3(person); // Person
    }
}

Java 和 Ruby 的对比

  1. 感觉 Java 比较严肃,对于开发不友好,但是对于程序的安全性、可靠性很友好;
  2. Ruby 给我的感觉是灵活、方便,对于开发非常友好,但是可靠性就差一点,因为有些错误只有在运行时才能暴露出来
  3. Java 有很多概念 Ruby 中没有,比如泛型,因为 Ruby 不需要…
  4. 语言没有最好,只有最合适!!!所以 PHP 是世界上最好的语言

代码地址

https://github.com/jiaxudonggit/JavaLearning

参考文档

https://pdai.tech/md/java/basic/java-basic-x-generic.html

https://www.liaoxuefeng.com/wiki/1252599548343744/1255945193293888

https://www.cnblogs.com/jpfss/p/9928747.html

https://www.cnblogs.com/coprince/p/8603492.html

0

评论区