Java N元组实现

26

我刚刚创建了一个Java n元组,它是类型安全的。
我使用了一些不寻常的方法来实现类型安全(只是出于好玩而已)。

有人能给出一些改进建议或可能存在的缺陷吗?

public class Tuple {
    private Object[] arr;
    private int size;
    private static boolean TypeLock = false;
    private static Object[] lastTuple = {1,1,1}; //default tuple type

    private Tuple(Object ... c) {
        // TODO Auto-generated constructor stub
        size=c.length;
        arr=c;
        if(TypeLock)
        {
            if(c.length == lastTuple.length)
                for(int i = 0; i<c.length; i++)
                {
                    if(c[i].getClass() == lastTuple[i].getClass())
                        continue;
                    else
                        throw new RuntimeException("Type Locked");
                }
            else
                throw new RuntimeException("Type Locked");
        }

        lastTuple = this.arr;
    }

    public static void setTypeLock(boolean typeLock) {
        TypeLock = typeLock;
    }

    @Override
    public boolean equals(Object obj) {
        // TODO Auto-generated method stub
        if (this == obj)
            return true;

        Tuple p = (Tuple)obj;

        for (int i = 0; i < size; i++)
        {
            if (p.arr[i].getClass() == this.arr[i].getClass())
            {
                if (!this.arr[i].equals(p.arr[i]))
                    return false;
            }
            else
                return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        // TODO Auto-generated method stub
        int res = 17;
        for(int i = 0; i < size; i++)
            res = res*37+arr[i].hashCode();

        return res;
    }

    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return Arrays.toString(arr);
    }

    public static void main(String[] args) {
        HashMap<Tuple,String> birthDay = new HashMap<Tuple,String>();
        Tuple p = new Tuple(1,2,1986);
        Tuple.setTypeLock(true);
        Tuple p2 = new Tuple(2,10,2009);
        Tuple p3 = new Tuple(1,2,2010);
        Tuple p4 = new Tuple(1,2,2010);
        birthDay.put(p,"Kevin");
        birthDay.put(p2,"Smith");
        birthDay.put(p3,"Sam");
        birthDay.put(p4, "Jack");
        System.out.println(birthDay);
        System.out.println(birthDay.get(new Tuple(1,2,1986)));
        birthDay.put(new Tuple(1,2,""),"");
    }
}

1
如何从元组中检索数据? - devoured elysium
5
元组可以包含具有不同类型的元素。因此,TypeLock和所有getClass相关的东西都没有意义。 - missingfaktor
3
我投票关闭此问题,因为该问题涉及对可用代码的代码审查;没有明确的问题陈述和解决方案。 - user719662
9个回答

49
称赞你学习通过实践。以下是改进的“机会”的建议:
  1. 只有一种Tuple可以存在(一旦Typelock被设置)。这会影响程序的可重用性和可扩展性,因为它们希望使用多种类型的元组,除非您采用剪贴复制重用(BirthdayTuple,DimensionsTuple,StreetAddressTuple等)。请考虑一个TupleFactory类,该类接受目标类型并创建一个元组构建器对象以生成元组。

  2. 在Tuple中,“null”值的有效性未经记录。我想在设置Typelock之前,允许null;但在设置Typelock之后,代码将生成NullPointerException-这是不一致的。如果不允许它们,则构造函数应捕获并禁止它们(无论Typelock如何)。如果允许它们,则整个代码(构造函数,equals,hashcode等)需要进行修改。

  3. 决定Tuple是否打算成为不可变值对象。基于其缺少setter方法,我猜测是这样的。如果是这样,请小心“采用”传入的数组-lastTuple = this.arr。即使它是一个var arg构造函数,构造函数也可以直接使用一个数组调用。 类采用数组(保留对它的引用),并且数组中的值可能在类之外被更改。我会复制数组的浅表副本,但也会记录具有不可变值的元组的潜在问题(可以在元组之外更改)。

  4. 您的equals方法缺少null检查(if(obj == null)return false)和类检查(obj instanceof Tuplethis.getClass().equals(object.getClass()))。等式成语很好地记录了。

  5. 没有办法查看Tuple的值,除非通过toString。这保护了值和整体的不可变性,但我认为它限制了类的实用性。

  6. 尽管我意识到这只是一个示例,但我不希望像生日/日期这样的东西使用此类。在具有固定对象类型的解决域中,真正的类(如Date)要好得多。我想象这个类对于元组是一等公民的特定领域是有用的。

编辑 一直在考虑这个问题。以下是一些代码(在github+tests上):

===
Tuple.java
===
package com.stackoverflow.tuple;

/**
 * Tuple are immutable objects.  Tuples should contain only immutable objects or
 * objects that won't be modified while part of a tuple.
 */
public interface Tuple {

    public TupleType getType();
    public int size();
    public <T> T getNthValue(int i);

}


===
TupleType.java
===
package com.stackoverflow.tuple;

/**
 * Represents a type of tuple.  Used to define a type of tuple and then
 * create tuples of that type.
 */
public interface TupleType {

    public int size();

    public Class<?> getNthType(int i);

    /**
     * Tuple are immutable objects.  Tuples should contain only immutable objects or
     * objects that won't be modified while part of a tuple.
     *
     * @param values
     * @return Tuple with the given values
     * @throws IllegalArgumentException if the wrong # of arguments or incompatible tuple values are provided
     */
    public Tuple createTuple(Object... values);

    public class DefaultFactory {
        public static TupleType create(final Class<?>... types) {
            return new TupleTypeImpl(types);
        }
    }

}


===
TupleImpl.java (not visible outside package)
===
package com.stackoverflow.tuple;

import java.util.Arrays;

class TupleImpl implements Tuple {

    private final TupleType type;
    private final Object[] values;

    TupleImpl(TupleType type, Object[] values) {
        this.type = type;
        if (values == null || values.length == 0) {
            this.values = new Object[0];
        } else {
            this.values = new Object[values.length];
            System.arraycopy(values, 0, this.values, 0, values.length);
        }
    }

    @Override
    public TupleType getType() {
        return type;
    }

    @Override
    public int size() {
        return values.length;
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> T getNthValue(int i) {
        return (T) values[i];
    }

    @Override
    public boolean equals(Object object) {
        if (object == null)   return false;
        if (this == object)   return true;

        if (! (object instanceof Tuple))   return false;

        final Tuple other = (Tuple) object;
        if (other.size() != size())   return false;

        final int size = size();
        for (int i = 0; i < size; i++) {
            final Object thisNthValue = getNthValue(i);
            final Object otherNthValue = other.getNthValue(i);
            if ((thisNthValue == null && otherNthValue != null) ||
                    (thisNthValue != null && ! thisNthValue.equals(otherNthValue))) {
                return false;
            }
        }

        return true;
    }

    @Override
    public int hashCode() {
        int hash = 17;
        for (Object value : values) {
            if (value != null) {
                hash = hash * 37 + value.hashCode();
            }
        }
        return hash;
    }

    @Override
    public String toString() {
        return Arrays.toString(values);
    }
}


===
TupleTypeImpl.java (not visible outside package)
===
package com.stackoverflow.tuple;

class TupleTypeImpl implements TupleType {

    final Class<?>[] types;

    TupleTypeImpl(Class<?>[] types) {
        this.types = (types != null ? types : new Class<?>[0]);
    }

    public int size() {
        return types.length;
    }

    //WRONG
    //public <T> Class<T> getNthType(int i)

    //RIGHT - thanks Emil
    public Class<?> getNthType(int i) {
        return types[i];
    }

    public Tuple createTuple(Object... values) {
        if ((values == null && types.length == 0) ||
                (values != null && values.length != types.length)) {
            throw new IllegalArgumentException(
                    "Expected "+types.length+" values, not "+
                    (values == null ? "(null)" : values.length) + " values");
        }

        if (values != null) {
            for (int i = 0; i < types.length; i++) {
                final Class<?> nthType = types[i];
                final Object nthValue = values[i];
                if (nthValue != null && ! nthType.isAssignableFrom(nthValue.getClass())) {
                    throw new IllegalArgumentException(
                            "Expected value #"+i+" ('"+
                            nthValue+"') of new Tuple to be "+
                            nthType+", not " +
                            (nthValue != null ? nthValue.getClass() : "(null type)"));
                }
            }
        }

        return new TupleImpl(this, values);
    }
}


===
TupleExample.java
===
package com.stackoverflow.tupleexample;

import com.stackoverflow.tuple.Tuple;
import com.stackoverflow.tuple.TupleType;

public class TupleExample {

    public static void main(String[] args) {

        // This code probably should be part of a suite of unit tests
        // instead of part of this a sample program

        final TupleType tripletTupleType =
            TupleType.DefaultFactory.create(
                    Number.class,
                    String.class,
                    Character.class);

        final Tuple t1 = tripletTupleType.createTuple(1, "one", 'a');
        final Tuple t2 = tripletTupleType.createTuple(2l, "two", 'b');
        final Tuple t3 = tripletTupleType.createTuple(3f, "three", 'c');
        final Tuple tnull = tripletTupleType.createTuple(null, "(null)", null);
        System.out.println("t1 = " + t1);
        System.out.println("t2 = " + t2);
        System.out.println("t3 = " + t3);
        System.out.println("tnull = " + tnull);

        final TupleType emptyTupleType =
            TupleType.DefaultFactory.create();

        final Tuple tempty = emptyTupleType.createTuple();
        System.out.println("\ntempty = " + tempty);

        // Should cause an error
        System.out.println("\nCreating tuple with wrong types: ");
        try {
            final Tuple terror = tripletTupleType.createTuple(1, 2, 3);
            System.out.println("Creating this tuple should have failed: "+terror);
        } catch (IllegalArgumentException ex) {
            ex.printStackTrace(System.out);
        }

        // Should cause an error
        System.out.println("\nCreating tuple with wrong # of arguments: ");
        try {
            final Tuple terror = emptyTupleType.createTuple(1);
            System.out.println("Creating this tuple should have failed: "+terror);
        } catch (IllegalArgumentException ex) {
            ex.printStackTrace(System.out);
        }

        // Should cause an error
        System.out.println("\nGetting value as wrong type: ");
        try {
            final Tuple t9 = tripletTupleType.createTuple(9, "nine", 'i');
            final String verror = t9.getNthValue(0);
            System.out.println("Getting this value should have failed: "+verror);
        } catch (ClassCastException ex) {
            ex.printStackTrace(System.out);
        }

    }

}

===
Sample Run
===
t1 = [1, one, a]
t2 = [2, two, b]
t3 = [3.0, three, c]
tnull = [null, (null), null]

tempty = []

Creating tuple with wrong types: 
java.lang.IllegalArgumentException: Expected value #1 ('2') of new Tuple to be class java.lang.String, not class java.lang.Integer
    at com.stackoverflow.tuple.TupleTypeImpl.createTuple(TupleTypeImpl.java:32)
    at com.stackoverflow.tupleexample.TupleExample.main(TupleExample.java:37)

Creating tuple with wrong # of arguments: 
java.lang.IllegalArgumentException: Expected 0 values, not 1 values
    at com.stackoverflow.tuple.TupleTypeImpl.createTuple(TupleTypeImpl.java:22)
    at com.stackoverflow.tupleexample.TupleExample.main(TupleExample.java:46)

Getting value as wrong type: 
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    at com.stackoverflow.tupleexample.TupleExample.main(TupleExample.java:58)

1
这个实现非常酷,如果我被允许的话,我会给予不止一票的支持。 - Emil
1
@Bert:昨天我刚看到这段代码,但无法运行。今天当我将代码粘贴到Eclipse时,对于getNthType函数的TupleTypeImpl出现了错误。然后我在接口中将方法签名与函数一样,并加上返回类型转换,它就可以工作了。这是因为我的JDK版本吗?我正在使用1.6。 - Emil
1
相当老的帖子,但是我不得不停下来赞扬这个优雅的解决方案+1。 - zcourts
3
这并不是一个好的解决方案。元组类型应该是强类型的,否则与仅使用对象数组相比没有太多好处。类型转换和异常不应该存在于元组实现中。可以参考.NET框架的实现方式来实现不原生支持元组的语言中的元组。 - intrepidis
1
没有足够的样板代码!:D 需要一些工厂bean和,也许,方面建议才能使这段代码真正适用于“生产”。(抱歉,我只是来找类似Python元组的东西) - Semyon Danilov
显示剩余8条评论

11

这种方法如何保证类型安全?你正在抛出运行时异常,而不是在编译时报告类型错误。

你正在尝试在静态类型语言中实现对元数的抽象,但目前还无法做到,同时又不失去类型安全。

补充:

元组可以包含异构元素(即具有不同类型的元素)。因此,即使对于这个Tuple类,提供“运行时类型安全”也是不可能的。该类的客户端负责进行适当的转换。

在Java中,这是你所能做的最好的办法: (编辑:请参见 Brent的帖子,了解更好的Tuple实现方式。(它不需要在客户端进行类型转换。))

final class Tuple {
  private final List<Object> elements;

  public Tuple(final Object ... elements) {
    this.elements = Arrays.asList(elements);
  }

  @Override
  public String toString() {
    return elements.toString();
  }

  //
  // Override 'equals' and 'hashcode' here
  //

  public Object at(final int index) {
    return elements.get(index);
  }
}

1
类型安全不一定要在编译时发生。但是在静态语言中,应该期望进行编译时类型检查... 好吧,强制转换无论如何都会规避这个问题。但第二段是正确的。 - user395760
1
@Emil:运行时类型安全?!对我来说听起来像一个糟糕的矛盾修辞。 - missingfaktor
1
@delnan:嗯?在静态类型语言中,任意元组的可能性是存在的。它们存在于所有类似ML的语言中(包括Haskell、OCaml和F#)。而Missing Faktor的补充并不能证明它们在Java中不可能存在。事实上,谷歌搜索“java tuple”会导向一个实现(与Emil的方法不同):http://adventuresinsoftware.com/blog/?p=546([源代码](http://github.com/mallardsoft/tuple))。 - Gilles 'SO- stop being evil'
1
@Gilles:是的,任意元数当然是可能的——我想说的是:元数始终是类型的一部分,例如(a, a)(a, a, a)是不同类型。 - user395760
2
@Missing:我是在回应delnan的评论。实际上,据我所知,在一个具有可决定静态类型系统的二阶段类型语言中抽象类型数量仍然是一个研究问题。(对于元组,一个简单的解决方法是使用嵌套对(如Coq中);但这并不是很实用(它倾向于导致元组存储为链接列表而不是向量,并且投影也不是很好)。) - Gilles 'SO- stop being evil'
显示剩余15条评论

6

这是最简单的解决方案,也是最好的方案。它类似于.NET中元组的表示方式。它巧妙地规避了Java类型擦除。它是强类型的,不会抛出异常,非常易于使用。

public interface Tuple
{
    int size();
}

public class Tuple2<T1,T2> implements Tuple
{
    public final T1 item1;
    public final T2 item2;

    public Tuple2(
        final T1 item_1,
        final T2 item_2)
    {
        item1 = item_1;
        item2 = item_2;
    }

    @Override
    public int size()
    {
        return 2;
    }
}

public class Tuple3<T1,T2,T3> implements Tuple
{
    public final T1 item1;
    public final T2 item2;
    public final T3 item3;

    public Tuple3(
        final T1 item_1,
        final T2 item_2,
        final T3 item_3)
    {
        item1 = item_1;
        item2 = item_2;
        item3 = item_3;
    }

    @Override
    public int size()
    {
        return 3;
    }
}

1
唯一的限制是随着 N 值的增加,您必须创建更多类型。这是语言的限制。元组应该内置到语言中,以感觉最自然。 - intrepidis
无论如何,如果你开始需要在元组中使用超过几个项目,那么你应该为实现创建一个具体类型,或者重构你的代码。 - intrepidis
1
是的,这实际上是处理元组的最佳方式,因为如果您需要更多的元素,应该使用列表或数组。 - Semyon Danilov

3

1
我想在Java中由于运行时类型擦除是不可能的。 - Abhinav Sarkar
2
尽管 .NET 中的所有元组类型都共享相同的名称(即 Tuple),但它们仍然被定义为不同的类型(就像在 Scala 中一样)。擦除唯一的问题是它不允许您拥有具有相同名称但不同类型参数数量的类(在 .NET 中的具体化类型可以让您这样做)。 - missingfaktor
我在Java中模仿了.NET元组的实现,链接在这里:http://intrepidis.blogspot.co.uk/2013/07/a-tuple-implementation-in-java.html - intrepidis

1
看到这个代码在 Wave 项目中。
public class Tuple<A> {

  private final A[] elements;

  public static <A> Tuple<A> of(A ... elements) {
    return new Tuple<A>(elements);
  }

  public Tuple(A ... elements) {
    this.elements = elements;
  }

  public A get(int index) {
    return elements[index];
  }

  public int size() {
    return elements.length;
  }

  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }

    if (o == null || o.getClass() != this.getClass()) {
      return false;
    }

    Tuple<A> o2 = (Tuple<A>) o;
    return Arrays.equals(elements, o2.elements);
  }

  @Override
  public int hashCode() {
    return Arrays.hashCode(elements);
  }

  @Override
  public String toString() {
    return Arrays.toString(elements);
  }
}

1

使用泛型可以更好地实现编译时类型安全。您可以为每个元数定义一个接口。然后,您可以定义单独的Callable接口来访问元组的值。

interface Tuple1 <T0> { <R> R accept ( Callable1<R,T0> callable ) ; }

interface Tuple2 <T0,T1> { <R> R accept ( Callable2<R,T0,T1> callable ) ; }

...

interface Tuplek <T0,T1,T2,...,Tk> { <R> R accept ( Callablek<R,T0,T1,T2,...,Tk> callable ) ; }

interface Callable1<R,T0> { R call ( T0 t0 ) ; }

interface Callable2<R,T0> { R call ( T0 t0 , T1 t1 ) ; }

....

interface Callablek<R,T0,T1,T2,...,Tk> { R call ( T0 t0 , T1 t1 , T2 t2 , ... , Tk tk ) ; }

1

typeLock 的目的是什么?允许某人防止构造更多这些对象吗?这部分不太合理。

为什么你会想要让某人阻止进一步实例化你的对象呢?如果出于某种原因,这是你需要的东西,那么不要“锁定”一个类并抛出异常,只需确保代码路径...不创建更多类型的对象即可。

静态 lastTuple 的目的是什么,它被设置为最后一个实例化的 Tuple 的引用?像这样混合使用静态引用是一种不好的做法。

坦白说,尽管对这个类的需求很困惑,但代码相当令人困惑。如果这是我在工作环境中审查的代码,我不会允许它存在。


不,这是为了防止使用不同类型进行实例化。只有最后一个实例的类型被锁定。就这样。一旦您完成了该类型,您可以重置类型锁并添加具有新类型的元组。 - Emil
看起来在单个JVM中只能有一种元组形状,而第一个构造的元组设置了该形状。这段代码毫无价值且愚蠢。 - Tom Anderson
1
@Emil,我认为你对“类型安全”有些困惑。这个锁似乎非常奇怪,我想不出任何理由为什么有人会想要拿一个容器类——旨在容纳任何类型——然后限制任何进一步的类实例被创建与不同类型参数的任何时间段内。你为什么要这样做呢? - matt b
我知道这只是一个实验,但我建议退一步,清楚地列出您想从Tuple类中获得的功能/责任/用途类型,然后编写该类以满足这些用例。您添加的一些“功能”似乎完全不必要,如果有什么,还是相反的。 - matt b

1
这是一个真正糟糕的n元组实现,使用泛型提供编译时类型检查。主要方法(仅用于演示目的)展示了使用它有多么可怕。
interface ITuple { }

/**
 * Typed immutable arbitrary-length tuples implemented as a linked list.
 *
 * @param <A> Type of the first element of the tuple
 * @param <D> Type of the rest of the tuple
 */
public class Tuple<A, D extends ITuple> implements ITuple {

    /** Final element of a tuple, or the single no-element tuple. */
    public static final TupleVoid END = new TupleVoid();

    /** First element of tuple. */
    public final A car;
    /** Remainder of tuple. */
    public final D cdr;

    public Tuple(A car, D cdr) {
        this.car = car;
        this.cdr = cdr;
    }

    private static class TupleVoid implements ITuple { private TupleVoid() {} }

    // Demo time!
    public static void main(String[] args) {
        Tuple<String, Tuple<Integer, Tuple<String, TupleVoid>>> triple =
                new Tuple<String, Tuple<Integer, Tuple<String, TupleVoid>>>("one",
                        new Tuple<Integer, Tuple<String, TupleVoid>>(2,
                                new Tuple<String, TupleVoid>("three",
                                        END)));
        System.out.println(triple.car + "/" + triple.cdr.car + "/" + triple.cdr.cdr.car);
        //: one/2/three
    }
}

0

如果你真的对编写类型安全的容器感兴趣,可以研究一下泛型:

public class Tuple<T> {
  private final T[] arr;
  public Tuple (T... contents) {
    arr = contents;  //not sure if this compiles??
  }

  // etc

  public static final void main(String[] args) {
    Tuple<String> stringTuple = new Tuple<String>("Hello", "World!");
    Tuple<Integer> intTuple = new Tuple<Integer>(2010,9,4);
  }
}

2
元组可以包含不同类型的元素。OP对元组的概念是错误的。 - missingfaktor
@missingfaktor:元组可以有不同的类型,因为它没有明确指定...但通常实现时会采用类型安全以便更容易和更安全地使用。我个人倾向于使它们类型安全,这样消费者就必须显式地选择一个更通用的类型,比如Object,如果他们想要混合使用不同类型。 - Jon Adams
@JonAdams,你确定你理解元组吗?它们基本上是由异构类型组成的固定长度复合结构。这里是链接 - missingfaktor
@missingfaktor:我显然看错了网上的资料。我查阅了其他几个网站的定义,并确认大多数人都同意它们应该保持一致的类型。 - Jon Adams

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接