【问题标题】:Java inheritence with different typed variables具有不同类型变量的 Java 继承
【发布时间】:2021-09-28 09:38:54
【问题描述】:

下面的代码有没有办法让这两个极其相似的类继承自一个超类?它们几乎只在变量类型上有所不同。如果没有,是否存在最适合这种情况的设计模式?我一直在考虑使用工厂模式,但我不确定这是否最适合这种情况。

class stringCharacteristic {
    String name = "";
    String value = "";

    public String getValue() {
        return value;
    }

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

    public String getName() {
        return name;
    }

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

class intCharacteristic {
    String name = "";
    int value = 0;

    public int getValue() {
        return value;
    }

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

    public String getName() {
        return name;
    }

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

编辑: 使用泛型找到了一个对我的目的相当有效的解决方案,可能不是最有效的,但它可以完成工作。另外,我知道代码在流畅的界面和所有东西上看起来有点不对劲,这不是我实际上正在编写的代码,只是为了显示问题而无需复制粘贴一百行只会分散主要注意力的内容问题。

class characteristic<E> {
    String name;
    E value;

    public E getValue() {
        return value;
    }

    public characteristic<E> setValue(E value) {
        this.value = value;
        return this;
    }

    public String getName() {
        return name;
    }

    public characteristic<E> setName(String name) {
        this.name = name;
        return this;
    }

    public characteristic<E> setMin(int min) {
        this.min = min;
        return this;
    }

    public characteristic<E> setMax(int max) {
        this.max = max;
        return this;
    }
}

【问题讨论】:

  • 你会寻找泛型,比如NamedWrapper&lt;T&gt;

标签: java inheritance design-patterns


【解决方案1】:

不(真的)。

这里有两个基本问题。

基元不玩动态输入游戏。

如果你有例如一个StringCharacteristic(注意在java中我们是WriteTypesLikeThis,而不是likeThis)和一个LocalDateCharacteristic,它们都是对象引用,我们可以对其进行概括,创建一个超类型ObjectCharacteristic&lt;T&gt;。但是,泛型不能是原始的,没有办法对原始进行泛化。

现在,就是这样。 Valhalla 项目正在进行大量的积极开发(我很确定它在很大程度上是目前在 OpenJDK 积极开发的所有各种与语言相关的项目中最努力的),所以请密切关注你的天气JDK 更新的功能列表。

您可以通过使用包装器类型 (java.lang.Integer) 来解决它,但这是一个非常糟糕的替代:它们可以是令人讨厌的 nullint 不能),它们的顺序是CPU 和内存方面的效率都大大降低。它们也不一定兼容 - 自动拆箱只能到此为止。

您可以将这些方法放入超类中(如果您不想将其作为公共 API 的一部分,请将其设为私有),使用泛型:class Characteristic&lt;T&gt;,使用 T 而不是int/String 在那个超类的任何地方,然后那个超类就可以了。如果你真的坚持有一个显式命名的类型,你可以写public class IntCharacteristic extends Characteristic&lt;Integer&gt; {},也许添加一个构造函数,然后一天调用它,剩下的就处理好了(甚至是你在超类中声明为的字段private T value.

但是,这为您提供了一个使用Integer 而不是int 的特征类。没有办法让它使用int。直到瓦尔哈拉。

如果您不愿意接受这种低效率,您将不得不手动写出与int 相关的位。

self类型的问题

如果你们都接受使用Integer 而不是int 的负面影响,则不会出现此次要问题,并且您可以只使用一个public class Characteristic&lt;T&gt;,使用没有自定义子类。

类型层次结构和“流式 API”(返回自己的类型的 API 方法)的问题在于它们在这里不起作用

插曲 - 让我们来写吧。

因为name位完全一样,除了setName方法的返回类型,我们可以这样做:

private abstract class Characteristic {
    private String name;

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

    public String getName() {
        return name;
    }
}

然后您可以编写它的子类型,并且您可以跳过名称内容的所有代码,因为您的超类会负责:

public class IntCharacteristic extends Characteristic {
    private int value;

    public IntCharacteristic setValue(int value) {
     ...
    }
}

但是,问题!

这里的一个问题是 setName 方法的返回类型错误。是的,它返回this,而this 将在新的IntCharacteristic 实例的情况下为instanceof IntCharacteristic,但是您的setName 方法的返回类型没有声明它,因此javac 不会如此对待它。因此,这将失败

new IntCharacteristic().setName("Hello").setValue(5);

您可以使用自泛型 hack 使其成功。

这是个坏主意,您需要继续阅读以获得更好的解决方案。我将其包含在此处是因为在这种情况下,虽然有更好的解决方案可用,但有时却没有,而这个 hack 是最好的解决方案:

public class Characteristic<S extends Characteristic<S>> {
    private String name;

    @SuppressWarnings("unchecked")
    protected S self() {
        return (S) this;
    }

    public S setName(String name) {
        this.name = name;
        return self();
    }
}

public class IntCharacteristic extends Characteristic<IntCharacteristic> {
}

此代码确保setName 的返回类型 IntCharacteristic,这是您想要的。 不可能在没有强制转换的情况下完成这项工作,这会导致编译器发出警告说该操作实际上根本不做任何检查,但我们知道这将是“真实的”,因为这个奇怪的签名(S extends Self&lt;S&gt;)实际上意味着任何扩展它的东西都只能把自己放在&lt;&gt;中。

更好的解决方案

大多数情况下,整个“回归自我”的东西都被夸大了。它不是特别的 java 风格,这显示了它是如何导致问题的。更一般地说,如果你坚持制作“现代”API,那么就像你建造了一个厨房,里面有一个中世纪的烤箱和一个全新的微波炉。现代的观点涉及不变性。为什么你甚至允许在这里更改名称?它不应该在创造时就被固定下来吗?那么你就避免了这整个混乱!

public class Characteristic {
    private final String name;

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

    public String getName() {
        return name;
    }
}

public class IntCharacteristic extends Characteristic {
    private final int value;

    public IntCharacteristic(String name, int value) {
        super(name);
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

问题解决了。

顺便说一句,不要再返回自己了

基本上,return this; 不是一个好主意,除非可能在 final 类中。问题在于,显然您的意图只是让“链式”方法调用变得容易,但如果涉及层次结构,则基本上 这不起作用,除非您使用上述不能“隐藏”的 hack(泛型参数现在是您的公共 API 的一部分)。

您也没有真正使用它来返回计算结果,而这正是返回值的目的。

换句话说,当你这样做时,你正在破解语言。这样做是有原因的,但最好睁大眼睛这样做:每当你破解语言时,你都必须面对这样一个事实,即它最终会咬你一口。

REAL 解决方案是让 java-the-language 发展“自我类型”的概念,或者更好的是,让 java-the-language 发展“allow chaining”的概念.这个问题的更好的解决方案是以下两个真正的解决方案之一:

语言建议 A

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

chainable 关键字都要求返回类型为void(如果不是编译器错误),并且它允许调用者编写new Foo().setName("stuff").chainMoreMethodsHere();。它纯粹是javac 语法糖,除了作为标记之外,这些都不会在类文件中存在,所以javac 知道该怎么做。类文件中的方法签名保持其 VOID 返回类型,语言工具知道这一点,例如在这里忽略返回类型完全没问题。

语言建议 B

把它交给调用者而不是 API 编写者:

只要有一个void方法:public void setName() { ... },但是调用者可以选择链式:

Foo x = new Foo()..setName()..setSomethingElse();

这里的双点表示:调用方法,丢弃返回的任何内容,并将表达式解析为与接收者相同。 (x.foo()表示:调用foo,折腾返回类型,表达式解析为x)。

目前,OpenJDK 列表中没有关于这方面的任何流量。但也许今天不一样了。如果添加了这个,你的 API 现在就坏了,可能会永远过时。

我并不是说流畅的 API 完全是白痴。但接近它。你正在破解语言,通常,无论是因为自我输入的东西还是未来的语言改进,这意味着你总有一天会后悔的。

【讨论】:

    【解决方案2】:

    这到底有多大用处取决于客户想要对Characteristic 的实例做什么,也就是说,当它无法分辨它们是什么具体类时。

    如果在某些情况下(例如)您想要显示一组 Characteristics 的名称,也许还有它们的值的 toString,那么这将很有用。

    你可以这样做:

    interface Characteristic<T> {
        public T getValue();
        public String getName();
        Characteristic<T> setValue(T value);
        Characteristic<T> setName(String name);
    }
    
    class StringCharacteristic implements Characteristic<String>{
        String name = "";
        String value = "";
    
        public String getValue() {
            return value;
        }
    
        public StringCharacteristic setValue(String value) {
            this.value = value;
            return this;
        }
    
        public String getName() {
            return name;
        }
    
        public StringCharacteristic setName(String name) {
            this.name = name;
            return this;
        }
    }
    
    class IntCharacteristic implements Characteristic<Integer> {
        String name = "";
        int value = 0;
    
        public Integer getValue() {
            return value;
        }
    
        public IntCharacteristic setValue(Integer value) {
            this.value = value;
            return this;
        }
    
        public String getName() {
            return name;
        }
    
        public IntCharacteristic setName(String name) {
            this.name = name;
            return this;
        }
    }
    

    (顺便请使用正确的Java命名约定)

    您可以使用抽象类来分解出通用实现:

    abstract class AbstractCharacteristic<T> implements Characteristic<T> {
        String name = "";
        T value;
    
        public AbstractCharacteristic(T value) {
            this.value = value;
        }
        
        public T getValue() {
            return value;
        }
    
        public Characteristic<T> setValue(T value) {
            this.value = value;
            return this;
        }
    
        public String getName() {
            return name;
        }
    
        public Characteristic<T> setName(String name) {
            this.name = name;
            return this;
        }
    }
    
    class StringCharacteristic extends AbstractCharacteristic<String>{
    
        public StringCharacteristic() {
            super("");
        }
    }
    
    class IntCharacteristic extends AbstractCharacteristic<Integer> {
    
        public IntCharacteristic() {
            super(0);
        }
    }
    

    (我们需要构造函数参数,因为默认情况下字符串字段通常会初始化为null,而不是""

    【讨论】:

      猜你喜欢
      • 2011-04-07
      • 2021-10-13
      • 2019-03-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-09-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多