考虑这个例子(在 OOP 书籍中很典型):
我有一个 Animal
类,其中每个 Animal
可以有很多朋友。
以及像 Dog
、Duck
、Mouse
等添加特定行为的子类,如 bark()
、quack()
等。
这是 Animal
类:
public class Animal {
private Map<String,Animal> friends = new HashMap<>();
public void addFriend(String name, Animal animal){
friends.put(name,animal);
}
public Animal callFriend(String name){
return friends.get(name);
}
}
这是一些带有大量类型转换的代码片段:
Mouse jerry = new Mouse();
jerry.addFriend("spike", new Dog());
jerry.addFriend("quacker", new Duck());
((Dog) jerry.callFriend("spike")).bark();
((Duck) jerry.callFriend("quacker")).quack();
有什么方法可以使用泛型作为返回类型来摆脱类型转换,所以我可以说
jerry.callFriend("spike").bark();
jerry.callFriend("quacker").quack();
这是一些初始代码,其返回类型作为从未使用过的参数传递给方法。
public<T extends Animal> T callFriend(String name, T unusedTypeObj){
return (T)friends.get(name);
}
有没有办法在运行时确定返回类型而无需使用 instanceof
的额外参数?或者至少通过传递一个类型的类而不是一个虚拟实例。
我知道泛型用于编译时类型检查,但有解决方法吗?
您可以这样定义 callFriend
:
public <T extends Animal> T callFriend(String name, Class<T> type) {
return type.cast(friends.get(name));
}
然后这样称呼它:
jerry.callFriend("spike", Dog.class).bark();
jerry.callFriend("quacker", Duck.class).quack();
此代码的好处是不会生成任何编译器警告。当然,这实际上只是从前通用时代开始的更新版本,并没有增加任何额外的安全性。
你可以像这样实现它:
@SuppressWarnings("unchecked")
public <T extends Animal> T callFriend(String name) {
return (T)friends.get(name);
}
(是的,这是法律法规;参见 Java Generics: Generic type defined as return type only。)
返回类型将从调用者推断。但是,请注意 @SuppressWarnings
注释:它告诉您此代码不是类型安全的。您必须自己验证它,否则您可能会在运行时获得 ClassCastExceptions
。
不幸的是,您使用它的方式(没有将返回值分配给临时变量),让编译器满意的唯一方法是这样调用它:
jerry.<Dog>callFriend("spike").bark();
虽然这可能比强制转换好一点,但正如 David Schmitt 所说,您最好为 Animal
类提供一个抽象的 talk()
方法。
jerry.CallFriend<Dog>(...
,我认为它看起来更好。
java.util.Collections.emptyList()
函数就是这样实现的,而且它的 javadoc 宣称自己是类型安全的。
Collections.emptyList()
可以逃脱的原因是每个空列表的定义,没有类型 T
的元素对象。因此没有将对象转换为错误类型的风险。只要没有元素,列表对象本身就可以使用任何类型。
不,编译器不知道 jerry.callFriend("spike")
会返回什么类型。此外,您的实现只是隐藏了方法中的强制转换,没有任何额外的类型安全。考虑一下:
jerry.addFriend("quaker", new Duck());
jerry.callFriend("quaker", /* unused */ new Dog()); // dies with illegal cast
在这种特定情况下,创建一个抽象 talk()
方法并在子类中适当地覆盖它会更好地为您服务:
Mouse jerry = new Mouse();
jerry.addFriend("spike", new Dog());
jerry.addFriend("quacker", new Duck());
jerry.callFriend("spike").talk();
jerry.callFriend("quacker").talk();
这个问题与 Effective Java 中的第 29 条非常相似——“考虑类型安全的异构容器”。 Laz 的答案最接近 Bloch 的解决方案。但是,为了安全起见, put 和 get 都应该使用 Class 字面量。签名将变为:
public <T extends Animal> void addFriend(String name, Class<T> type, T animal);
public <T extends Animal> T callFriend(String name, Class<T> type);
在这两种方法中,您应该检查参数是否正常。有关详细信息,请参阅 Effective Java 和 Class javadoc。
这是更简单的版本:
public <T> T callFriend(String name) {
return (T) friends.get(name); //Casting to T not needed in this case but its a good practice to do
}
完整的工作代码:
public class Test {
public static class Animal {
private Map<String,Animal> friends = new HashMap<>();
public void addFriend(String name, Animal animal){
friends.put(name,animal);
}
public <T> T callFriend(String name){
return (T) friends.get(name);
}
}
public static class Dog extends Animal {
public void bark() {
System.out.println("i am dog");
}
}
public static class Duck extends Animal {
public void quack() {
System.out.println("i am duck");
}
}
public static void main(String [] args) {
Animal animals = new Animal();
animals.addFriend("dog", new Dog());
animals.addFriend("duck", new Duck());
Dog dog = animals.callFriend("dog");
dog.bark();
Duck duck = animals.callFriend("duck");
duck.quack();
}
}
Casting to T not needed in this case but it's a good practice to do
是什么。我的意思是,如果在运行时得到妥善处理,“良好做法”是什么意思?
正如你所说的通过一个类就可以了,你可以这样写:
public <T extends Animal> T callFriend(String name, Class<T> clazz) {
return (T) friends.get(name);
}
然后像这样使用它:
jerry.callFriend("spike", Dog.class).bark();
jerry.callFriend("quacker", Duck.class).quack();
并不完美,但这几乎是您使用 Java 泛型所能获得的。有一种实现 Typesafe Heterogenous Containers (THC) using Super Type Tokens 的方法,但它又存在自己的问题。
基于与 Super Type Tokens 相同的想法,您可以创建一个类型化的 id 来代替字符串:
public abstract class TypedID<T extends Animal> {
public final Type type;
public final String id;
protected TypedID(String id) {
this.id = id;
Type superclass = getClass().getGenericSuperclass();
if (superclass instanceof Class) {
throw new RuntimeException("Missing type parameter.");
}
this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
}
}
但我认为这可能会破坏目的,因为您现在需要为每个字符串创建新的 id 对象并保留它们(或使用正确的类型信息重建它们)。
Mouse jerry = new Mouse();
TypedID<Dog> spike = new TypedID<Dog>("spike") {};
TypedID<Duck> quacker = new TypedID<Duck>("quacker") {};
jerry.addFriend(spike, new Dog());
jerry.addFriend(quacker, new Duck());
但是您现在可以按照您最初想要的方式使用该类,而无需强制转换。
jerry.callFriend(spike).bark();
jerry.callFriend(quacker).quack();
这只是将类型参数隐藏在 id 中,尽管这确实意味着您可以稍后根据需要从标识符中检索类型。
如果您希望能够比较一个 id 的两个相同实例,您还需要实现 TypedID 的比较和散列方法。
“有没有办法在运行时确定返回类型而无需使用 instanceof 的额外参数?”
作为替代解决方案,您可以像这样使用 the Visitor pattern。使 Animal 抽象并使其实现 Visitable:
abstract public class Animal implements Visitable {
private Map<String,Animal> friends = new HashMap<String,Animal>();
public void addFriend(String name, Animal animal){
friends.put(name,animal);
}
public Animal callFriend(String name){
return friends.get(name);
}
}
Visitable 只是意味着 Animal 实现愿意接受访问者:
public interface Visitable {
void accept(Visitor v);
}
访问者实现能够访问动物的所有子类:
public interface Visitor {
void visit(Dog d);
void visit(Duck d);
void visit(Mouse m);
}
因此,例如 Dog 实现将如下所示:
public class Dog extends Animal {
public void bark() {}
@Override
public void accept(Visitor v) { v.visit(this); }
}
这里的技巧是,当 Dog 知道它是什么类型时,它可以通过传递“this”作为参数来触发访问者 v 的相关重载访问方法。其他子类将以完全相同的方式实现 accept() 。
想要调用子类特定方法的类必须实现 Visitor 接口,如下所示:
public class Example implements Visitor {
public void main() {
Mouse jerry = new Mouse();
jerry.addFriend("spike", new Dog());
jerry.addFriend("quacker", new Duck());
// Used to be: ((Dog) jerry.callFriend("spike")).bark();
jerry.callFriend("spike").accept(this);
// Used to be: ((Duck) jerry.callFriend("quacker")).quack();
jerry.callFriend("quacker").accept(this);
}
// This would fire on callFriend("spike").accept(this)
@Override
public void visit(Dog d) { d.bark(); }
// This would fire on callFriend("quacker").accept(this)
@Override
public void visit(Duck d) { d.quack(); }
@Override
public void visit(Mouse m) { m.squeak(); }
}
我知道它的接口和方法比您预想的要多得多,但它是一种标准方法,可以通过精确的零实例检查和零类型转换来处理每个特定子类型。而且这一切都是以与标准语言无关的方式完成的,因此它不仅适用于 Java,而且任何 OO 语言都应该以同样的方式工作。
不可能。仅给定一个 String 键, Map 应该如何知道它将获得 Animal 的哪个子类?
唯一可行的方法是,如果每个 Animal 只接受一种类型的朋友(那么它可能是 Animal 类的参数),或者 callFriend() 方法的类型参数。但看起来你真的错过了继承的重点:只有在只使用超类方法时,你才能统一对待子类。
我写了一篇文章,其中包含一个概念证明、支持类和一个测试类,它演示了您的类如何在运行时检索超级类型令牌。简而言之,它允许您根据调用者传递的实际通用参数委托替代实现。例子:
TimeSeries
TimeSeries
看:
使用 TypeTokens 检索泛型参数(网络存档)
使用 TypeTokens 检索泛型参数(博客)
谢谢
理查德·戈麦斯 - Blog
这里有很多很好的答案,但这是我在 Appium 测试中采用的方法,其中对单个元素进行操作可能会导致根据用户设置进入不同的应用程序状态。虽然它不遵循 OP 示例的约定,但我希望它对某人有所帮助。
public <T extends MobilePage> T tapSignInButton(Class<T> type) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//signInButton.click();
return type.getConstructor(AppiumDriver.class).newInstance(appiumDriver);
}
MobilePage 是类型扩展的超类,这意味着您可以使用它的任何子级(duh)
type.getConstructor(Param.class, etc) 允许您与类型的构造函数进行交互。这个构造函数在所有预期的类之间应该是相同的。
newInstance 接受一个你想要传递给新对象构造函数的声明变量
如果你不想抛出错误,你可以像这样捕获它们:
public <T extends MobilePage> T tapSignInButton(Class<T> type) {
// signInButton.click();
T returnValue = null;
try {
returnValue = type.getConstructor(AppiumDriver.class).newInstance(appiumDriver);
} catch (Exception e) {
e.printStackTrace();
}
return returnValue;
}
不是真的,因为正如您所说,编译器只知道 callFriend() 返回的是 Animal,而不是 Dog 或 Duck。
你能不能向 Animal 添加一个抽象的 makeNoise() 方法,它会被它的子类实现为 bark 或 quack?
您在这里寻找的是抽象。更多针对接口的代码,您应该减少强制转换。
下面的示例使用 C#,但概念保持不变。
using System;
using System.Collections.Generic;
using System.Reflection;
namespace GenericsTest
{
class MainClass
{
public static void Main (string[] args)
{
_HasFriends jerry = new Mouse();
jerry.AddFriend("spike", new Dog());
jerry.AddFriend("quacker", new Duck());
jerry.CallFriend<_Animal>("spike").Speak();
jerry.CallFriend<_Animal>("quacker").Speak();
}
}
interface _HasFriends
{
void AddFriend(string name, _Animal animal);
T CallFriend<T>(string name) where T : _Animal;
}
interface _Animal
{
void Speak();
}
abstract class AnimalBase : _Animal, _HasFriends
{
private Dictionary<string, _Animal> friends = new Dictionary<string, _Animal>();
public abstract void Speak();
public void AddFriend(string name, _Animal animal)
{
friends.Add(name, animal);
}
public T CallFriend<T>(string name) where T : _Animal
{
return (T) friends[name];
}
}
class Mouse : AnimalBase
{
public override void Speak() { Squeek(); }
private void Squeek()
{
Console.WriteLine ("Squeek! Squeek!");
}
}
class Dog : AnimalBase
{
public override void Speak() { Bark(); }
private void Bark()
{
Console.WriteLine ("Woof!");
}
}
class Duck : AnimalBase
{
public override void Speak() { Quack(); }
private void Quack()
{
Console.WriteLine ("Quack! Quack!");
}
}
}
我在我的 lib kontraktor 中执行了以下操作:
public class Actor<SELF extends Actor> {
public SELF self() { return (SELF)_self; }
}
子类化:
public class MyHttpAppSession extends Actor<MyHttpAppSession> {
...
}
至少这在当前类中以及在具有强类型引用时有效。多重继承有效,但变得非常棘手:)
我知道这是一个完全不同的人问的事情。解决这个问题的另一种方法是反射。我的意思是,这并没有从泛型中受益,但它可以让你以某种方式模拟你想要执行的行为(让狗吠,让鸭子嘎嘎等等),而不需要照顾类型转换:
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
abstract class AnimalExample {
private Map<String,Class<?>> friends = new HashMap<String,Class<?>>();
private Map<String,Object> theFriends = new HashMap<String,Object>();
public void addFriend(String name, Object friend){
friends.put(name,friend.getClass());
theFriends.put(name, friend);
}
public void makeMyFriendSpeak(String name){
try {
friends.get(name).getMethod("speak").invoke(theFriends.get(name));
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
public abstract void speak ();
};
class Dog extends Animal {
public void speak () {
System.out.println("woof!");
}
}
class Duck extends Animal {
public void speak () {
System.out.println("quack!");
}
}
class Cat extends Animal {
public void speak () {
System.out.println("miauu!");
}
}
public class AnimalExample {
public static void main (String [] args) {
Cat felix = new Cat ();
felix.addFriend("Spike", new Dog());
felix.addFriend("Donald", new Duck());
felix.makeMyFriendSpeak("Spike");
felix.makeMyFriendSpeak("Donald");
}
}
由于问题是基于假设数据,这里是一个很好的例子,它返回了一个扩展 Comparable 接口的泛型。
public class MaximumTest {
// find the max value using Comparable interface
public static <T extends Comparable<T>> T maximum(T x, T y, T z) {
T max = x; // assume that x is initially the largest
if (y.compareTo(max) > 0){
max = y; // y is the large now
}
if (z.compareTo(max) > 0){
max = z; // z is the large now
}
return max; // returns the maximum value
}
//testing with an ordinary main method
public static void main(String args[]) {
System.out.printf("Maximum of %d, %d and %d is %d\n\n", 3, 4, 5, maximum(3, 4, 5));
System.out.printf("Maximum of %.1f, %.1f and %.1f is %.1f\n\n", 6.6, 8.8, 7.7, maximum(6.6, 8.8, 7.7));
System.out.printf("Maximum of %s, %s and %s is %s\n", "strawberry", "apple", "orange",
maximum("strawberry", "apple", "orange"));
}
}
还有另一种方法,您可以在覆盖方法时缩小返回类型。在每个子类中,您必须重写 callFriend 以返回该子类。代价是 callFriend 的多个声明,但您可以将公共部分隔离到内部调用的方法中。这对我来说似乎比上面提到的解决方案简单得多,并且不需要额外的参数来确定返回类型。
public int getValue(String name){}
与 public boolean getValue(String name){}
无法区分。您需要更改参数类型或添加/删除参数才能识别重载。也许我只是误解了你...
关于什么
public class Animal {
private Map<String,<T extends Animal>> friends = new HashMap<String,<T extends Animal>>();
public <T extends Animal> void addFriend(String name, T animal){
friends.put(name,animal);
}
public <T extends Animal> T callFriend(String name){
return friends.get(name);
}
}
不定期副业成功案例分享