初识Java 18-6 泛型

news2024/11/14 13:33:19

目录

潜在类型机制

支持潜在类型机制的语言

Python的潜在类型机制

C++的潜在类型机制

Java中的直接潜在类型机制

潜在类型机制的替代方案

反射

将方法应用于序列中的每个元素

Java 8的潜在类型机制(间接实现)

潜在类型机制的使用例(Suppliers)

总结


本笔记参考自: 《On Java 中文版》


潜在类型机制

        通过泛型,我们应该可以向“将代码写得更通用一点”这一理念更进一步。特别是在编写简单的Java泛型时,泛型可以在不了解具体类型的情况下执行方法。然而,随着对Java泛型的了解逐渐深入,类型擦除无疑使泛型的作用打了折扣,并限制了“泛型”这一概念。

        一些语言会提供潜在类型机制(又称结构化类型机制),它还有一个更有意思的名称:鸭子类型机制。比方说,“如果某件事物走路像鸭子,说话也像鸭子,那么就可以把它看做鸭子”。

    与泛型不同,潜在类型机制只对方法本身有所要求,而不需要实现特别的类或接口。

        潜在类型机制可以超越类的层次结构,调用不属于某个公共接口的方法。

支持潜在类型机制的语言

        有许多语言支持潜在类型机制,例如Python、C++和Go等。

Python的潜在类型机制

        先看看Python中的潜在类型机制:

【例子:Python中的潜在类型机制】

class Dog:
    def speak(self):
        print("汪!")
    def sit(self):
        print("坐着")
    def reproduce(self):
        pass
    
class Robot:
    def speak(self):
        print("锵!")
    def sit(self):
        print("咔")
    def oilChange(self):
        pass
    
def perform(anything):
    anything.speak()
    anything.sit()
    
a = Dog()
b = Robot()
perform(a)
perform(b)

        程序执行的结果是:

        在perform(anything)中并不包含任何关于anything的类型信息,anything只是一个标识符。在后台,anything相当于一个被隐藏的接口,它包含了perform()要求的操作。但我们无需显式地表明它,因为它是潜在的。

        而如果向perform()传入不支持操作的对象,就会报错:

----------

C++的潜在类型机制

        同样可以用C++来实现上面的例子:

【例子:C++中的潜在类型机制】

#include<iostream>
using namespace std;

class Dog {
public:
    void speak() {
        cout << "汪!" << endl;
    }

    void sit() {
        cout << "坐着" << endl;
    }
};

class Robot {
public:
    void speak() {
        cout << "锵!" << endl;
    }

    void sit() {
        cout << "咔" << endl;
    }

    void oilChange() {}
};

template<class T> void perform(T anything) {
    anything.speak();
    anything.sit();
}

int main() {
    Dog d;
    Robot r;
    perform(d);
    perform(r);
}

        程序执行的结果相同:

        若试图传入错误的类型,编译器也会报错。不同与Python,C++会在运行时抛出错误(尽管C++的错误信息出了名的冗长)。但这两门语言都保证了类型不会错用。

----------

        也可以使用Go实现这个例子:

【例子:Go中的潜在类型机制】

package main
import "fmt"

type Dog struct{}
func (this Dog) speak() {
	fmt.Printf("汪!\n")
}
func (this Dog) sit(){
	fmt.Printf("坐着\n")
}
func (this Dog) reproduce(){
}

type Robot struct{}
func (this Robot) speak() {
	fmt.Printf("锵!\n")
}
func (this Robot) sit(){
	fmt.Printf("咔\n")
}
func (this Robot) oilChange(){
}

func perform(speaker interface {speak(); sit()}){
	speaker.speak()
	speaker.sit()
}

func main(){
	perform(Dog{})
	perform(Robot{})
}

        程序会得到相同的结果:


Java中的直接潜在类型机制

        Java的泛型加入得较晚,因此没有支持潜在类型机制。因此,若要在Java中实现上面所述的效果,通常会需要使用接口(并且使用边界):

【例子:通过接口模拟潜在类型机制】

import reflection.pets.Dog;

class PerformingDog extends Dog implements Performs {
    @Override
    public void speak() {
        System.out.println("汪!");
    }

    @Override
    public void sit() {
        System.out.println("坐下");
    }

    public void reproduce() {
    }
}

class Robot implements Performs {
    @Override
    public void speak() {
        System.out.println("锵!");
    }

    @Override
    public void sit() {
        System.out.println("咔");
    }

    public void oilChange() {
    }
}

class Communicate {
    // 通过边界,调用接口的方法:
    public static <T extends Performs>
    void perform(T performer) {
        performer.speak();
        performer.sit();
    }
}

public class DogsAndRobots {
    public static void main(String[] args) {
        Communicate.perform(new PerformingDog());
        Communicate.perform(new Robot());
    }
}

        程序执行的结果是:

        然而,仔细考虑这种做法会发现,Communicate.perform()并不需要使用到泛型,它可以直接使用Performs接口:

class Communicate {
    public static void perform(Performs performer) {
        performer.speak();
        performer.sit();
    }
}

说到底,无论是PerformingDog还是Robot都已经强制实现了Performs接口。

潜在类型机制的替代方案

        尽管Java并没有(直接)支持潜在类型机制,但我们依旧可以想办法创建出真正意义上的泛型代码,实现方法的跨层次应用。

反射

        一个方案是使用反射:

【例子:使用反射创建泛型代码】

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

// 这个类没有实现Perform接口:
class Mime {
    public void walkAgainstTheWind() {
    }

    public void sit() {
        System.out.println("假装坐着");
    }

    public void pushInvisibleWalls() {
    }

    @Override
    public String toString() {
        return "哑剧";
    }
}

class SmartDog {
    public void speak() {
        System.out.println("汪!");
    }

    public void sit() {
        System.out.println("坐下");
    }

    public void reproduce() {
    }
}

class CommunicateReflectively {
    public static void perform(Object speaker) {
        Class<?> spkr = speaker.getClass();
        try {
            try {
                Method speak = spkr.getMethod("speak");
                speak.invoke(speaker);
            } catch (NoSuchMethodException e) {
                System.out.println(speaker + "没法说话");
            }

            try {
                Method sit = spkr.getMethod("sit");
                sit.invoke(speaker);
            } catch (NoSuchMethodException e) {
                System.out.println(speaker + "无法坐下");
            }
        } catch (SecurityException |
                 IllegalAccessException |
                 IllegalArgumentException |
                 InvocationTargetException e) {
            throw new RuntimeException(speaker.toString(), e);
        }
    }
}

public class LatentReflection {
    public static void main(String[] args) {
        CommunicateReflectively.perform(new SmartDog());
        CommunicateReflectively.perform(new Robot());
        CommunicateReflectively.perform(new Mime());
    }
}

        程序执行的结果是:

        在这里,SmartDogRobotMime之间没有任何的直接联系,我们直接通过反射动态调用speak()sit()


将方法应用于序列中的每个元素

        还可以进一步地开发反射。假设我们需要为一个序列中的每个元素应用方法,只使用接口仍是不够的,因此我们可以这样做:

【例子:将一个方法应用于序列】

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Apply {
    public static <T, S extends Iterable<T>>
    void apply(S seq, Method f, Object... args) {
        try {
            for (T t : seq)
                f.invoke(t, args);
        } catch (IllegalAccessException |
                 IllegalArgumentException |
                 InvocationTargetException e) {
            throw new RuntimeException(e); // 可能会因为不正确的使用该方法导致异常
        }
    }
}

        apply()方法可以接受任意数量的序列元素,并将方法f()应用于所有元素。这种做法有一个好处,f.invoke()可以接受任意长度的序列,因此可以认为apply()也可以做到这点。

    除此之外,apply()方法使用了for-in语法。 这表示着S可以是任何实现了Iterable接口的类。

        接下来就对apply()方法进行测试:

【例子:apply()方法的使用例】

        首先创建一个简单的继承结构,这个结构包含一个父类Shape和一个子类Square

===父类Shape

public class Shape {
    private static long counter = 0;
    private final long id = counter++;

    @Override
    public String toString() {
        return getClass().getSimpleName() + " " + id;
    }

    public void rotate() {
        System.out.println(this + " rotate");
    }

    public void resize(int newSize) {
        System.out.println(this + " resize " + newSize);
    }
}

==子类Square

public class Square extends Shape {}

        接下来的就是测试类ApplyTest

===ApplyTest

import onjava.Suppliers;

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

public class ApplyTest {
    public static void main(String[] args)
            throws Exception {
        List<Shape> shapes =
                Suppliers.create(ArrayList::new, Shape::new, 3);
        Apply.apply(shapes,
                Shape.class.getMethod("rotate"));
        Apply.apply(shapes,
                Shape.class.getMethod("resize", int.class), 7);

        System.out.println();
        List<Square> squares =
                Suppliers.create(ArrayList::new, Square::new, 3);
        Apply.apply(squares,
                Shape.class.getMethod("rotate"));
        Apply.apply(squares,
                Shape.class.getMethod("resize", int.class), 7);

        System.out.println();
        Apply.apply(new FilledList<>(Shape::new, 3),
                Shape.class.getMethod("rotate"));
        Apply.apply(new FilledList<>(Square::new, 3),
                Shape.class.getMethod("rotate"));
    }
}

        程序执行的结果是:

        首先解释Suppliers.create(),看如下代码:

这行代码等价于,将Shape类的构造器作为生成器,生成3个对象,并将结果放入一个ArrayList对象中。

        尽管反射可以使代码看起来很优雅,但要注意,反射的运行速度通常会慢于非反射的实现。究其原因,反射在运行时处理了太多东西。尽管我们不应该只因为这个理由放弃使用反射,但这无疑是我们必须考虑的一点。

        现在考虑Java 8加入的函数式方式,下面的例子通过它重写ApplyTest

【例子:使用函数式方式重写ApplyTest

import java.util.stream.Stream;

public class ApplyFunctional {
    public static void main(String[] args) {
        Stream.of(Stream.generate(Shape::new).limit(2),
                        Stream.generate(Square::new).limit(2))
                .flatMap(c -> c) // 将所有元素扁平化,合成一条流
                .peek(Shape::rotate)
                .forEach(s -> s.resize(7));

        System.out.println();
        new FilledList<>(Shape::new, 2)
                .forEach(Shape::rotate);
        new FilledList<>(Square::new, 2)
                .forEach(Shape::rotate);
    }
}

        程序执行的结果是:

        这种重写方式已经抛弃了Apply.apply()了。

        这么做的好处是,它更加简洁、可读性高并且不会抛出异常。因此现在可以这么说,我们只需要在某些只能使用反射来解决的场景中使用反射就好了。

Java 8的潜在类型机制(间接实现)

        Java 8带来的未绑定方法引用可以在某种程度上实现潜在类型机制。

【例子:使用方法引用实现潜在类型机制】

import reflection.pets.Dog;

import java.util.function.Consumer;

class PerformingDogA extends Dog {
    public void speak() {
        System.out.println("汪!");
    }

    public void sit() {
        System.out.println("坐下");
    }

    public void reproduce() {
    }
}

class RobotA {
    public void speak() {
        System.out.println("锵!");
    }

    public void sit() {
        System.out.println("咔");
    }

    public void oilChange() {
    }
}

class CommunicateA {
    public static <P> void perform(
            P performer, Consumer<P> action1, Consumer<P> action2) {
        action1.accept(performer);
        action2.accept(performer);
    }
}

public class DogsAndRobotMethodReferences {
    public static void main(String[] args) {
        CommunicateA.perform(new PerformingDogA(),
                PerformingDogA::speak, PerformingDogA::sit);
        CommunicateA.perform(new RobotA(),
                RobotA::speak, RobotA::sit);
        CommunicateA.perform(new Mime(),
                Mime::walkAgainstTheWind, Mime::pushInvisibleWalls);
    }
}

        程序执行结果是:

        因为CommunicateA.perform()没有对P做出限制,因此它可以是任何类型。perform()只要求提供可供Consumer<P>使用的方法。因此我们可以向其中传入任何与其签名一致的方法

    然而,和真正的潜在类型机制相比,这种做法需要我们显式地提供perform()会用到的方法引用。

        但这种方式也有更好的一点,它其实不会对传入其中的方法名做出要求。在这个意义上,这种方法要更加通用。

潜在类型机制的使用例(Suppliers)

        现在可以通过潜在类型机制创建Suppliers(这个类在之前的笔记中也曾出现)。这个类的方法都用于填充集合:

【例子:潜在类型机制的使用例】

import java.util.Collection;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.stream.Stream;

public class Suppliers {
    // 根据factory创建一个新的集合,并且填充它:
    public static <T, C extends Collection<T>> C
    create(Supplier<C> factory, Supplier<T> gen, int n) {
        return Stream.generate(gen)
                .limit(n)
                .collect(factory, C::add, C::addAll);
    }

    // 填充已存在的集合coll:
    public static <T, C extends Collection<T>>
    C fill(C coll, Supplier<T> gen, int n) {
        Stream.generate(gen)
                .limit(n)
                .forEach(coll::add);
        return coll;
    }

    // 使用到未绑定方法引用adder,可以生成更加通用的方法:
    public static <H, A> H fill(
            H holder, BiConsumer<H, A> adder,
            Supplier<A> gen, int n) {
        Stream.generate(gen)
                .limit(n)
                .forEach(a -> adder.accept(holder, a));
        return holder;
    }
}

        在第一个fill()方法中,我们返回了coll(即传入的容器的类型信息),这样就能保证不会丢失类型信息了。

        第二个fill()方法使用了未绑定的方法引用adder。通过adder.accept(),我们可以将操作a应用于对象holder

        接下来可以尝试使用Suppliers了。

【例子:使用Suppliers

import onjava.Suppliers;

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

class Customer {
    private static long counter = 1;
    private final long id = counter++;

    @Override
    public String toString() {
        return "Customer " + id;
    }
}

class Teller {
    private static long counter = 1;
    private final long id = counter++;

    @Override
    public String toString() {
        return "Teller " + id;
    }
}

class Bank {
    private List<BankTeller> tellers =
            new ArrayList<>();

    public void put(BankTeller bt) {
        tellers.add(bt);
    }
}

public class BankTeller {
    public static void serve(Teller t, Customer c) {
        System.out.println(t + " 服务于 " + c);
    }

    public static void main(String[] args) {
        // 使用create():
        RandomList<Teller> tellers =
                Suppliers.create(
                        RandomList::new, Teller::new, 4);

        // 演示第一个fill():
        List<Customer> customers = Suppliers.fill(
                new ArrayList<>(), Customer::new, 12);
        customers.forEach(c ->
                serve(tellers.select(), c));

        // 演示潜在类型机制:
        Bank bank = Suppliers.fill(
                new Bank(), Bank::put, BankTeller::new, 3);
        // 或使用第二个fill():
        List<Customer> customers2 = Suppliers.fill(
                new ArrayList<>(), List::add, Customer::new, 12);
    }
}

        程序执行的结果是:

        注意第二个fill()的两个使用,我们不仅可以将有所关联的Bank类型,还可以将其用于List

总结

        尽管Java中加入的泛型存在着一些问题,但在此之前我们需要注意一点:无论一门语言多么强大,都有可能被用来编写一个无比糟糕的程序。泛型常常被用于集合之类的场景,但除此之外,泛型还能做到什么?

        泛型的概念应该就像它的名字一样,追求的是一种更加“泛型(泛化)”的代码,使一种代码能够适应更多的场景。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1271642.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

项目终验的exce表格缩放,排版等经常使用

xxx个项目的验收资料 1.申请表等等很多信息 需求&#xff1a;放在一页内等办法 上述文档&#xff0c;在excel表格打印预览中都是在两页中&#xff0c;很难调节&#xff0c;这个时候采用wps专业版本即可。 wps排版经常使用的功能如下&#xff1a; 经常使用的是 1.把所有列打印…

基于SSM校园驿站管理系统的设计与实现

摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对校园快递信息管理混乱&#xff0c;出错率高&#xff0c;信息安全性差…

【tower-boot 系列】MybatisPlus 集成

Mybatis、MybatisPlus 简单介绍 MybatisPlus 集成 一、pom 依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId><exclusions><exclusion><groupId>com.zaxxer…

Windows安装Kafka3.6,单机

Kafka版本&#xff1a;kafka_2.13-3.6.0 Windows10系统 安装与配置 下载 kafka_2.13-3.6.0.tgz 下载并解压Kafka 3.6.0的压缩包到你选择的目录。 Kafka3.6.0下载链接https://kafka.apache.org/downloads 说明&#xff1a;Kafka3.6内置了Zookeeper&#xff0c;使用内置的Zo…

基于SSM的松田学院在线考试管理系统的设计与实现

在线考试系统的设计与实现 摘要&#xff1a;传统考试模式有许多弊端&#xff0c;大部分学校的考试大都釆用的是传统的考试方式&#xff0c;卷子少发了&#xff0c;考试人数不方便数清楚&#xff0c;答题错了还要换纸等等,而试卷纸是重要的环保资源。随着科技的进步&#xff0c…

PC端数据列表有头像显示头像,没有头像显示名字的第一个字

PC端数据列表有头像显示头像&#xff0c;没有头像显示名字的第一个字 .charAt(0) 是 JavaScript 字符串对象的方法&#xff0c;用于获取字符串的第一个字符。 字符串中的字符位置是从 0 开始的&#xff0c;所以.charAt(0) 就表示获取字符串的第一个字符。 <el-table ref&qu…

拓数派荣获上海市“智慧工匠”工业软件创新案例奖

近日&#xff0c;由上海市经济和信息化委员会指导、上海市城市数字化转型应用促进中心主办、上海中创产业创新研究院承办的“工业软件赋能新型工业化”主题沙龙暨2023“智慧工匠”工业软件创新案例竞赛颁奖典礼在上海圆满落幕。拓数派凭借上汽集团工业数据管理服务平台案例成功…

关于前端学习的思考-内边距、边框和外边距

从最简单的盒子开始思考 先把实际应用摆出来&#xff1a; margin&#xff1a;居中&#xff0c;控制边距。 padding&#xff1a;控制边距。 border&#xff1a;制作三角形。 盒子分为内容盒子&#xff0c;内边距盒子&#xff0c;边框和外边距。 如果想让块级元素居中&#…

Mybatis批处理数据插入(rewriteBatchedStatements参数)

一、rewriteBatchedStatements参数 1、MySQL JDBC驱动在默认情况下会无视executeBatch()【也就是说JDBC默认情况下&#xff0c;会将你的语句分拆成单个&#xff0c;一条一条发给数据库执行&#xff0c;数据量小时感知不大&#xff0c;1w或10w以上差距越来越大】 2、MySQL的JDBC…

如何使用Windows自带的IIS服务搭建本地站点并远程访问

文章目录 1.前言2.Windows网页设置2.1 Windows IIS功能设置2.2 IIS网页访问测试 3. Cpolar内网穿透3.1 下载安装Cpolar内网穿透3.2 Cpolar云端设置3.3 Cpolar本地设置 4.公网访问测试5.结语 1.前言 在网上各种教程和介绍中&#xff0c;搭建网页都会借助各种软件的帮助&#xf…

扫码听音乐该如何制作?音乐的二维码生成方法

多个音频文件怎么做成一个二维码显示&#xff1f;二维码在现在的生活中拥有丰富的使用场景&#xff0c;可以用来作为多种内容类型的载体&#xff0c;比如音频二维码就是经常被使用的一种二维码类型。通过扫秒二维码来听音频文件&#xff0c;更加的灵活方便&#xff0c;那么音频…

python中的函数定义

默认参数 注&#xff1a; 在Python中&#xff0c;print(x, and y both correct)是一条打印语句&#xff08;print statement&#xff09;&#xff0c;用于将一条消息输出到控制台或终端。它的作用是将变量x的值和字符串and y both correct同时输出到屏幕上。 在这个语句中&…

基于YOLOv8深度学习的火焰烟雾检测系统【python源码+Pyqt5界面+数据集+训练代码】目标检测、深度学习实战

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

Flat Ads将携6亿独家流量亮相白鲸GTC2023,在7V01展台等你

一年一度的白鲸出海全球流量大会GTC重磅来袭!今年GTC出海展区全面升级,规模扩增至15000平方米,覆盖游戏、应用、技术及品牌出海等热门行业,预计将迎来累计超30000名跨境出海相关从业者莅临参观。 Flat Ads受邀设展,现场互动100%中奖 从出海到全球化,中国互联网企业走向海外寻…

「Python编程基础」第5章:列表

文章目录 一、为什么要有列表&#xff1f;二、列表语法三、用索引获取列表中的单个值四、利用切片取得子列表五、利用len()函数&#xff0c;获取列表的长度六、利用索引改变列表中的值七、列表的连接和复制八、用del语句删除列表中的值九、有了列表后&#xff0c;真香十、列表的…

解决:uniapp项目打包微信小程序时,报错:failed to load config from /xx/xx-mall/vite.config.ts

复现步骤&#xff1a;在vscode终端中运行&#xff1a;pnpm build:mp-weixin-prod 命令&#xff0c;打包小程序生产包时&#xff0c;报错failed to load xxx/vite.config.ts&#xff0c;但实际项目根目录中有该vite.config.ts文件。 项目使用技术&#xff1a;uniapp vue3 node…

2023年第三届中国高校大数据挑战赛思路及代码

比赛时间&#xff1a;2023.12.28 08:00 至 2023.12.31 20:00 赛题方向介绍 1、大数据统计分析方向 涉及内容包含&#xff1a;数据的清洗、数据的预测、数据之间的关联分析、综合评价、分类与判别等 2、文本或图象分析方向 涉及内容包含&#xff1a;计算机视觉基础、特征匹配…

记录labelImg上手过程

一、安装 Labelimg&#xff08;目标检测标注工具&#xff09;安装_labelimg安装_向南不向北的博客-CSDN博客 二、打开 进入anaconda虚拟环境后&#xff0c;cd到labelimg文件夹&#xff0c;然后输入命令 python labelImg.py 三、基础设置 打标工具labelimg安装和使用教程-C…

前端三大MV*模式:MVC、mvvm、mvp模式介绍

MVC&#xff08;同步通信为主&#xff09;&#xff1a;Model、View、Controller MVP(异步通信为主)&#xff1a;Model、View、Presenter MVVM(异步通信为主)&#xff1a;Model、View、ViewModel mvc模式介绍 MVC&#xff08;Model–View–Controller&#xff09;模式是软件…

CCS中静态库lib的生成与调用

在调试DSP设备的时候&#xff0c;发现好多工程会把比较核心的代码生成静态库lib&#xff0c;代码运行的时候直接调用lib里面的相关函数就行。但是从外部是看不到lib库里面的内容的&#xff0c;这样通过静态库的方式实现对代码的加密。 在网上找了好久如何将函数生成静态库*.lib…