【问题标题】:Preserving encapsulation of a generic in Java在 Java 中保留泛型的封装
【发布时间】:2013-09-27 04:25:15
【问题描述】:

晚上好。

我有一个相当复杂的问题。为了练习 Java,我一直在重新实现标准库中的一些数据结构。 Stacks、LinkedLists、Trees 等。我刚刚通过一个非常简单的示例确定,当使用peek()pop() 方法时,java.util.Stack 类执行深层复制。这是可以理解的,因为目标是保护课程内容免受外界干扰。到目前为止,在我自己的 Stack 实现中(一个简单的数组的幼稚实现,链表会在后面出现),我根本没有关心这个:

public class ArrayStack<T> implements Stack<T> {
    private T[] data; // Will expand the array when stack is full.
    private int top; // serves as both top and count indicator.
    ...
    ...
   @Override
   public T pop() throws EmptyStackException {
    if(top == -1)
        throw new EmptyStackException("Stack is empty.");
    return data[top--]; // Shallow copy, dangerous!
}

不幸的是,由于无法实例化泛型,因此我不能假设复制构造函数并执行 return new T(data[top--]); 之类的事情clone() 的变体。 This 线程建议将类的签名扩展为:

public class ArrayStack<T extends DeepCloneableClass> implements Stack<T>
...

其中DeepCloneableClass 是一个实现允许“深度克隆”的接口的类(有关详细信息,请参阅该线程中的顶部响应)。当然,这种方法的问题在于,我真的不能指望StringInteger 等标准类扩展我的自定义类,当然,我所有现有的 jUnit 测试现在都是在编译时抱怨,因为它们依赖于这样的整数和字符串堆栈。所以我觉得这个解决方案不可行。

This 线程建议使用第三方库来克隆几乎任何对象。虽然这个库似乎仍然受支持(最新的错误修复日期不到一个月前),但我宁愿不依赖第三方工具,而是使用 Java 可以为我提供的任何东西。原因是这些 ADT 的源代码有朝一日可能会与本​​科生共享,我不希望他们承担安装额外工具的负担。

因此,我正在寻找一种简单且尽可能有效的方法来维护通用 Java 数据结构的内部完整性,同时仍然允许与 pop()peek()popFront() 等方法的简单接口.

非常感谢您的帮助!

杰森

【问题讨论】:

    标签: java oop generics clone type-safety


    【解决方案1】:

    为什么需要克隆对象?

    您的堆栈只有一个引用集合。您可能不需要克隆它们,只需创建一个新数组并将适当的引用放入其中,然后丢弃旧数组即可。

    【讨论】:

    • 我需要克隆对象的原因是,如果您从上面的代码中注意到,在实现 pop() 时,我返回了对堆栈中第一个对象的引用。理想情况下,我想要的是深拷贝,但不幸的是,我不能使用带有泛型的拷贝构造函数!通过创建一个新数组,您是否暗示我应该只保留对最终返回的顶部元素的引用,然后重新创建一个缺少对顶部元素引用的新数组?这听起来可能有效,但效率极低。
    • 这很好 - 您可能希望返回对堆栈中第一个对象的引用。集合不会复制对象,因为开发人员可能希望为每个有引用的人更改对象。您需要保留对第一个对象的引用,然后创建一个包含对所有其他对象的现有引用的新数组。
    • 我想确认您刚才所说的,关于 Java 标准库中的集合实际上是如何制作引用的深层副本。我暂时忘记了字符串和整数是不可变的,因此对它们的引用并不能真正影响它们。对我感到困惑的事情的完整回答将添加到自我回答中。谢谢一百万!
    • 您绝对不想克隆存储在堆栈中的对象。使用您的堆栈的开发人员会期望,如果她将一个对象放在堆栈上,她将能够稍后再次获得完全相同的实例,而不是它的副本。而且,如果她同时选择以某种方式变异该对象,那么稍后弹出的将是变异对象,而不是原始对象的副本。但是,您可能需要考虑线程安全在您的实现中是否很重要。
    • @Jason:不,集合不会复制其内容。这与对象无关——它与引用有关。集合存储引用;他们指向什么对象,他们真的不在乎。事实上,在 Java 中没有通用的方法来制作对象的副本。
    【解决方案2】:

    IntegerStrings 等都是不可变的,因此它们的内容在设计上是安全的。

    对于自定义对象,虽然有经验的 Java 程序员肯定会对此有不同的感受,但implementing a custom interface 无疑是解决问题的一种方法。

    另一种是制作&lt;T extends Serializable&gt;(由IntegerString等实现)和"clone" through serialization

    但是,如果您想以“正确的方式”教您的学生,我肯定会使用第三方库...您只需在项目中创建一个 lib 文件夹并配置您的构建工具/IDE 以将所需的 jar 添加到Classpath 使用相对路径,因此您的本科生无需安装或设置任何东西。

    仅供参考,this question 可能很有用。

    我一直在使用这种方法教授 Java 入门课程(作为 IT 讲师/不是作为大学教授),而且它不像听起来那么痛苦。

    【讨论】:

    • 确实,字符串和整数是不可变的这一事实让我想到了一个例子,它让我相信标准库对返回值进行了深层复制,这是一个错误的假设。我将在自我回复中详细说明我做错了什么。谢谢!
    • 我还看到许多不同的地方都在使用克隆库。我会更详细地查找它。
    • 你会的,加上GuavaApache Commons 等库肯定会让你编写更简洁的代码。请不要忘记接受 Jason 或我的回答,以便 stackoverflow 社区可以从我们的经验中受益。
    【解决方案3】:

    cmets 帮助我了解我做错了什么。我正在使用以下示例向自己和其他人“证明”Java 标准库的集合在提供对集合中对象的引用时会进行深层复制:

    import java.util.Stack;
    
    public class StackTestDeepCopy {
        public static void main(String[] args){
            Stack<String> st = new Stack<String>();
            st.push("Jim");
            st.push("Jill");
            String top = st.peek();
            top = "Jack";
            System.out.println(st); 
        }
    }
    

    打印 st 时,我看到对象没有改变,并得出结论,发生了深拷贝。错误的! Strings 是不可变的,因此声明 top = "Jack" 不会以任何方式修改 String (并不是说任何 Object 都会被这样的声明“修改”,但我没有直截了当) ,它只是将参考点指向堆上的一个新位置。一个涉及实际可变类的新示例使我以自己的方式理解了错误。

    现在这个问题已经解决了,我对标准库允许这样做的事实感到很困惑。为什么访问标准库中的元素被实现为浅拷贝?听起来很不安全。

    【讨论】:

    • 这不是浅拷贝——它只是返回你一开始压入堆栈的引用。您只是在 peek() 之后的行中为“top”分配了一个全新的对象引用。浅拷贝意味着别的东西。
    • 为什么访问标准库中的元素被实现为浅拷贝?不是,它返回对对象的引用。当你不需要可变性时,你可以设计不可变的对象。
    【解决方案4】:

    java.util.Stack 不做深拷贝:

    import java.util.Stack;
    public class Test {
        String foo;
        public static void main(String[] args) {
            Test test = new Test();
            test.foo = "bar";
            Stack<Test> stack = new Stack<Test>();
            stack.push(test);
            Test otherTest = stack.pop();
            otherTest.foo = "wibble";
            System.out.println("Are the same object: "+(test.foo == otherTest.foo));
        }
    }
    

    结果:

    Are the same object: true
    

    如果它确实进行了复制,那么 test 和 otherTest 将指向不同的对象。典型的堆栈实现只是返回对添加到堆栈中的同一对象的引用,而不是副本。


    您可能还希望在返回之前将数组项设置为 null,否则该数组仍将持有对该对象的引用。

    【讨论】:

    • 是的,我弄清楚了浅拷贝的情况。是的,我不允许垃圾收集通过不“取消”引用来启动。感谢您的指点,没有双关语。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-20
    • 2019-09-13
    • 1970-01-01
    • 2021-06-01
    相关资源
    最近更新 更多