提交 a0fd01b5 编写于 作者: M mduigou

6312706: Map entrySet iterators should return different entries on each call to next()

Reviewed-by: mduigou, alanb
Contributed-by: NNeil Richards <neil.richards@ngmr.net>
上级 fa996007
......@@ -106,7 +106,7 @@ public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
/**
* Distinguished non-null value for representing null values.
*/
private static final Object NULL = new Object();
private static final Object NULL = new Integer(0);
private Object maskNull(Object value) {
return (value == null ? NULL : value);
......@@ -116,7 +116,7 @@ public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
return (V) (value == NULL ? null : value);
}
private static Enum[] ZERO_LENGTH_ENUM_ARRAY = new Enum[0];
private static final Enum[] ZERO_LENGTH_ENUM_ARRAY = new Enum[0];
/**
* Creates an empty enum map with the specified key type.
......@@ -464,6 +464,7 @@ public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
public Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
public boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
......@@ -552,70 +553,82 @@ public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
}
}
/**
* Since we don't use Entry objects, we use the Iterator itself as entry.
*/
private class EntryIterator extends EnumMapIterator<Map.Entry<K,V>>
implements Map.Entry<K,V>
{
private class EntryIterator extends EnumMapIterator<Map.Entry<K,V>> {
private Entry lastReturnedEntry = null;
public Map.Entry<K,V> next() {
if (!hasNext())
throw new NoSuchElementException();
lastReturnedIndex = index++;
return this;
lastReturnedEntry = new Entry(index++);
return lastReturnedEntry;
}
public K getKey() {
checkLastReturnedIndexForEntryUse();
return keyUniverse[lastReturnedIndex];
public void remove() {
lastReturnedIndex =
((null == lastReturnedEntry) ? -1 : lastReturnedEntry.index);
super.remove();
lastReturnedEntry.index = lastReturnedIndex;
lastReturnedEntry = null;
}
public V getValue() {
checkLastReturnedIndexForEntryUse();
return unmaskNull(vals[lastReturnedIndex]);
}
private class Entry implements Map.Entry<K,V> {
private int index;
public V setValue(V value) {
checkLastReturnedIndexForEntryUse();
V oldValue = unmaskNull(vals[lastReturnedIndex]);
vals[lastReturnedIndex] = maskNull(value);
return oldValue;
}
private Entry(int index) {
this.index = index;
}
public boolean equals(Object o) {
if (lastReturnedIndex < 0)
return o == this;
public K getKey() {
checkIndexForEntryUse();
return keyUniverse[index];
}
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
V ourValue = unmaskNull(vals[lastReturnedIndex]);
Object hisValue = e.getValue();
return e.getKey() == keyUniverse[lastReturnedIndex] &&
(ourValue == hisValue ||
(ourValue != null && ourValue.equals(hisValue)));
}
public V getValue() {
checkIndexForEntryUse();
return unmaskNull(vals[index]);
}
public int hashCode() {
if (lastReturnedIndex < 0)
return super.hashCode();
public V setValue(V value) {
checkIndexForEntryUse();
V oldValue = unmaskNull(vals[index]);
vals[index] = maskNull(value);
return oldValue;
}
Object value = vals[lastReturnedIndex];
return keyUniverse[lastReturnedIndex].hashCode()
^ (value == NULL ? 0 : value.hashCode());
}
public boolean equals(Object o) {
if (index < 0)
return o == this;
public String toString() {
if (lastReturnedIndex < 0)
return super.toString();
if (!(o instanceof Map.Entry))
return false;
return keyUniverse[lastReturnedIndex] + "="
+ unmaskNull(vals[lastReturnedIndex]);
}
Map.Entry e = (Map.Entry)o;
V ourValue = unmaskNull(vals[index]);
Object hisValue = e.getValue();
return (e.getKey() == keyUniverse[index] &&
(ourValue == hisValue ||
(ourValue != null && ourValue.equals(hisValue))));
}
private void checkLastReturnedIndexForEntryUse() {
if (lastReturnedIndex < 0)
throw new IllegalStateException("Entry was removed");
public int hashCode() {
if (index < 0)
return super.hashCode();
return entryHashCode(index);
}
public String toString() {
if (index < 0)
return super.toString();
return keyUniverse[index] + "="
+ unmaskNull(vals[index]);
}
private void checkIndexForEntryUse() {
if (index < 0)
throw new IllegalStateException("Entry was removed");
}
}
}
......@@ -631,10 +644,35 @@ public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
* @return <tt>true</tt> if the specified object is equal to this map
*/
public boolean equals(Object o) {
if (!(o instanceof EnumMap))
return super.equals(o);
if (this == o)
return true;
if (o instanceof EnumMap)
return equals((EnumMap)o);
if (!(o instanceof Map))
return false;
Map<K,V> m = (Map<K,V>)o;
if (size != m.size())
return false;
for (int i = 0; i < keyUniverse.length; i++) {
if (null != vals[i]) {
K key = keyUniverse[i];
V value = unmaskNull(vals[i]);
if (null == value) {
if (!((null == m.get(key)) && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
}
return true;
}
EnumMap em = (EnumMap)o;
private boolean equals(EnumMap em) {
if (em.keyType != keyType)
return size == 0 && em.size == 0;
......@@ -649,6 +687,26 @@ public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
return true;
}
/**
* Returns the hash code value for this map. The hash code of a map is
* defined to be the sum of the hash codes of each entry in the map.
*/
public int hashCode() {
int h = 0;
for (int i = 0; i < keyUniverse.length; i++) {
if (null != vals[i]) {
h += entryHashCode(i);
}
}
return h;
}
private int entryHashCode(int index) {
return (keyUniverse[index].hashCode() ^ vals[index].hashCode());
}
/**
* Returns a shallow copy of this enum map. (The values themselves
* are not cloned.
......@@ -705,9 +763,13 @@ public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
s.writeInt(size);
// Write out keys and values (alternating)
for (Map.Entry<K,V> e : entrySet()) {
s.writeObject(e.getKey());
s.writeObject(e.getValue());
int entriesToBeWritten = size;
for (int i = 0; entriesToBeWritten > 0; i++) {
if (null != vals[i]) {
s.writeObject(keyUniverse[i]);
s.writeObject(unmaskNull(vals[i]));
entriesToBeWritten--;
}
}
}
......
/*
* Copyright (c) 2000, 2008, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2000, 2011, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
......@@ -829,71 +829,82 @@ public class IdentityHashMap<K,V>
}
}
/**
* Since we don't use Entry objects, we use the Iterator
* itself as an entry.
*/
private class EntryIterator
extends IdentityHashMapIterator<Map.Entry<K,V>>
implements Map.Entry<K,V>
{
private Entry lastReturnedEntry = null;
public Map.Entry<K,V> next() {
nextIndex();
return this;
lastReturnedEntry = new Entry(nextIndex());
return lastReturnedEntry;
}
public K getKey() {
// Provide a better exception than out of bounds index
if (lastReturnedIndex < 0)
throw new IllegalStateException("Entry was removed");
return (K) unmaskNull(traversalTable[lastReturnedIndex]);
public void remove() {
lastReturnedIndex =
((null == lastReturnedEntry) ? -1 : lastReturnedEntry.index);
super.remove();
lastReturnedEntry.index = lastReturnedIndex;
lastReturnedEntry = null;
}
public V getValue() {
// Provide a better exception than out of bounds index
if (lastReturnedIndex < 0)
throw new IllegalStateException("Entry was removed");
private class Entry implements Map.Entry<K,V> {
private int index;
return (V) traversalTable[lastReturnedIndex+1];
}
private Entry(int index) {
this.index = index;
}
public V setValue(V value) {
// It would be mean-spirited to proceed here if remove() called
if (lastReturnedIndex < 0)
throw new IllegalStateException("Entry was removed");
V oldValue = (V) traversalTable[lastReturnedIndex+1];
traversalTable[lastReturnedIndex+1] = value;
// if shadowing, force into main table
if (traversalTable != IdentityHashMap.this.table)
put((K) traversalTable[lastReturnedIndex], value);
return oldValue;
}
public K getKey() {
checkIndexForEntryUse();
return (K) unmaskNull(traversalTable[index]);
}
public boolean equals(Object o) {
if (lastReturnedIndex < 0)
return super.equals(o);
public V getValue() {
checkIndexForEntryUse();
return (V) traversalTable[index+1];
}
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
return e.getKey() == getKey() &&
e.getValue() == getValue();
}
public V setValue(V value) {
checkIndexForEntryUse();
V oldValue = (V) traversalTable[index+1];
traversalTable[index+1] = value;
// if shadowing, force into main table
if (traversalTable != IdentityHashMap.this.table)
put((K) traversalTable[index], value);
return oldValue;
}
public int hashCode() {
if (lastReturnedIndex < 0)
return super.hashCode();
public boolean equals(Object o) {
if (index < 0)
return super.equals(o);
return System.identityHashCode(getKey()) ^
System.identityHashCode(getValue());
}
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
return (e.getKey() == unmaskNull(traversalTable[index]) &&
e.getValue() == traversalTable[index+1]);
}
public String toString() {
if (lastReturnedIndex < 0)
return super.toString();
public int hashCode() {
if (lastReturnedIndex < 0)
return super.hashCode();
return getKey() + "=" + getValue();
return (System.identityHashCode(unmaskNull(traversalTable[index])) ^
System.identityHashCode(traversalTable[index+1]));
}
public String toString() {
if (index < 0)
return super.toString();
return (unmaskNull(traversalTable[index]) + "="
+ traversalTable[index+1]);
}
private void checkIndexForEntryUse() {
if (index < 0)
throw new IllegalStateException("Entry was removed");
}
}
}
......
/*
* Copyright (c) 2011 Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* Portions Copyright (c) 2011 IBM Corporation
*/
/*
* @test
* @bug 6312706
* @summary Sets from Map.entrySet() return distinct objects for each Entry
* @author Neil Richards <neil.richards@ngmr.net>, <neil_richards@uk.ibm.com>
*/
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class DistinctEntrySetElements {
static enum TestEnum { e00, e01, e02 }
public static void main(String[] args) throws Exception {
final EnumMap<TestEnum, String> enumMap = new EnumMap<>(TestEnum.class);
for (TestEnum e : TestEnum.values()) {
enumMap.put(e, e.name());
}
Set<Map.Entry<TestEnum, String>> entrySet = enumMap.entrySet();
HashSet<Map.Entry<TestEnum, String>> hashSet = new HashSet<>(entrySet);
if (false == hashSet.equals(entrySet)) {
throw new RuntimeException("Test FAILED: Sets are not equal.");
}
if (hashSet.hashCode() != entrySet.hashCode()) {
throw new RuntimeException("Test FAILED: Set's hashcodes are not equal.");
}
}
}
/*
* Copyright (c) 2011 Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* Portions Copyright (c) 2011 IBM Corporation
*/
/*
* @test
* @bug 6312706
* @summary Iterator.remove() from Map.entrySet().iterator() invalidates returned Entry.
* @author Neil Richards <neil.richards@ngmr.net>, <neil_richards@uk.ibm.com>
*/
import java.util.EnumMap;
import java.util.Iterator;
import java.util.Map;
public class EntrySetIteratorRemoveInvalidatesEntry {
static enum TestEnum { e00, e01, e02 }
public static void main(String[] args) throws Exception {
final EnumMap<TestEnum, String> enumMap = new EnumMap<>(TestEnum.class);
for (TestEnum e : TestEnum.values()) {
enumMap.put(e, e.name());
}
Iterator<Map.Entry<TestEnum, String>> entrySetIterator =
enumMap.entrySet().iterator();
Map.Entry<TestEnum, String> entry = entrySetIterator.next();
entrySetIterator.remove();
try {
entry.getKey();
throw new RuntimeException("Test FAILED: Entry not invalidated by removal.");
} catch (Exception e) { }
}
}
/*
* Copyright (c) 2011 Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* Portions Copyright (c) 2011 IBM Corporation
*/
/*
* @test
* @bug 6312706
* @summary A serialized EnumMap can be successfully de-serialized.
* @author Neil Richards <neil.richards@ngmr.net>, <neil_richards@uk.ibm.com>
*/
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.EnumMap;
public class SimpleSerialization {
private enum TestEnum { e00, e01, e02, e03, e04, e05, e06, e07 }
public static void main(final String[] args) throws Exception {
final EnumMap<TestEnum, String> enumMap = new EnumMap<>(TestEnum.class);
enumMap.put(TestEnum.e01, TestEnum.e01.name());
enumMap.put(TestEnum.e04, TestEnum.e04.name());
enumMap.put(TestEnum.e05, TestEnum.e05.name());
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(enumMap);
oos.close();
final byte[] data = baos.toByteArray();
final ByteArrayInputStream bais = new ByteArrayInputStream(data);
final ObjectInputStream ois = new ObjectInputStream(bais);
final Object deserializedObject = ois.readObject();
ois.close();
if (false == enumMap.equals(deserializedObject)) {
throw new RuntimeException(getFailureText(enumMap, deserializedObject));
}
}
private static String getFailureText(final Object orig, final Object copy) {
final StringWriter sw = new StringWriter();
final PrintWriter pw = new PrintWriter(sw);
pw.println("Test FAILED: Deserialized object is not equal to the original object");
pw.print("\tOriginal: ");
printObject(pw, orig).println();
pw.print("\tCopy: ");
printObject(pw, copy).println();
pw.close();
return sw.toString();
}
private static PrintWriter printObject(final PrintWriter pw, final Object o) {
pw.printf("%s@%08x", o.getClass().getName(), System.identityHashCode(o));
return pw;
}
}
/*
* Copyright (c) 2011 Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* Portions Copyright (c) 2011 IBM Corporation
*/
/*
* @test
* @bug 6312706
* @summary Sets from Map.entrySet() return distinct objects for each Entry
* @author Neil Richards <neil.richards@ngmr.net>, <neil_richards@uk.ibm.com>
*/
import java.util.IdentityHashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class DistinctEntrySetElements {
public static void main(String[] args) throws Exception {
final IdentityHashMap<String, String> identityHashMap =
new IdentityHashMap<>();
identityHashMap.put("One", "Un");
identityHashMap.put("Two", "Deux");
identityHashMap.put("Three", "Trois");
Set<Map.Entry<String, String>> entrySet = identityHashMap.entrySet();
HashSet<Map.Entry<String, String>> hashSet = new HashSet<>(entrySet);
// NB: These comparisons are valid in this case because none of the
// keys put into 'identityHashMap' above are equal to any other.
if (false == hashSet.equals(entrySet)) {
throw new RuntimeException("Test FAILED: Sets are not equal.");
}
if (hashSet.hashCode() != entrySet.hashCode()) {
throw new RuntimeException("Test FAILED: Set's hashcodes are not equal.");
}
}
}
/*
* Copyright (c) 2011 Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* Portions Copyright (c) 2011 IBM Corporation
*/
/*
* @test
* @bug 6312706
* @summary Iterator.remove() from Map.entrySet().iterator() invalidates returned Entry.
* @author Neil Richards <neil.richards@ngmr.net>, <neil_richards@uk.ibm.com>
*/
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
public class EntrySetIteratorRemoveInvalidatesEntry {
public static void main(String[] args) throws Exception {
final IdentityHashMap<String, String> identityHashMap =
new IdentityHashMap<>();
identityHashMap.put("One", "Un");
identityHashMap.put("Two", "Deux");
identityHashMap.put("Three", "Trois");
Iterator<Map.Entry<String, String>> entrySetIterator =
identityHashMap.entrySet().iterator();
Map.Entry<String, String> entry = entrySetIterator.next();
entrySetIterator.remove();
try {
entry.getKey();
throw new RuntimeException("Test FAILED: Entry not invalidated by removal.");
} catch (Exception e) { }
}
}
/*
* Copyright (c) 2011 Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* Portions Copyright (c) 2011 IBM Corporation
*/
/*
* @test
* @bug 6312706
* @summary Sets from Map.entrySet() return distinct objects for each Entry
* @author Neil Richards <neil.richards@ngmr.net>, <neil_richards@uk.ibm.com>
*/
import java.util.concurrent.ConcurrentHashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class DistinctEntrySetElements {
public static void main(String[] args) throws Exception {
final ConcurrentHashMap<String, String> concurrentHashMap =
new ConcurrentHashMap<>();
concurrentHashMap.put("One", "Un");
concurrentHashMap.put("Two", "Deux");
concurrentHashMap.put("Three", "Trois");
Set<Map.Entry<String, String>> entrySet = concurrentHashMap.entrySet();
HashSet<Map.Entry<String, String>> hashSet = new HashSet<>(entrySet);
if (false == hashSet.equals(entrySet)) {
throw new RuntimeException("Test FAILED: Sets are not equal.");
}
if (hashSet.hashCode() != entrySet.hashCode()) {
throw new RuntimeException("Test FAILED: Set's hashcodes are not equal.");
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册