Item 18: Favor composition over inheritance(優(yōu)先選擇復(fù)合而不是繼承)

Inheritance is a powerful way to achieve code reuse, but it is not always the best tool for the job. Used inappropriately, it leads to fragile software. It is safe to use inheritance within a package, where the subclass and the superclass implementations are under the control of the same programmers. It is also safe to use inheritance when extending classes specifically designed and documented for extension (Item 19). Inheriting from ordinary concrete classes across package boundaries, however, is dangerous. As a reminder, this book uses the word “inheritance” to mean implementation inheritance (when one class extends another). The problems discussed in this item do not apply to interface inheritance (when a class implements an interface or when one interface extends another).

繼承是實(shí)現(xiàn)代碼復(fù)用的一種強(qiáng)大方法,但它并不總是最佳的工具。使用不當(dāng)會導(dǎo)致軟件變得脆弱。在包中使用繼承是安全的,其中子類和超類實(shí)現(xiàn)由相同的程序員控制。在對專為擴(kuò)展而設(shè)計(jì)和文檔化的類時(shí)使用繼承也是安全的(Item-19)。然而,對普通的具體類進(jìn)行跨包邊界的繼承是危險(xiǎn)的。作為提醒,本書使用「繼承」一詞來表示實(shí)現(xiàn)繼承(當(dāng)一個(gè)類擴(kuò)展另一個(gè)類時(shí))。本條目中討論的問題不適用于接口繼承(當(dāng)類實(shí)現(xiàn)接口或一個(gè)接口擴(kuò)展另一個(gè)接口時(shí))。

Unlike method invocation, inheritance violates encapsulation [Snyder86]. In other words, a subclass depends on the implementation details of its superclass for its proper function. The superclass’s implementation may change from release to release, and if it does, the subclass may break, even though its code has not been touched. As a consequence, a subclass must evolve in tandem with its superclass, unless the superclass’s authors have designed and documented it specifically for the purpose of being extended.

與方法調(diào)用不同,繼承破壞了封裝 [Snyder86]。換句話說,子類的功能正確與否依賴于它的超類的實(shí)現(xiàn)細(xì)節(jié)。超類的實(shí)現(xiàn)可能在版本之間發(fā)生變化,如果發(fā)生了變化,子類可能會崩潰,即使子類的代碼沒有被修改過。因此,子類必須與其超類同步發(fā)展,除非超類是專門為擴(kuò)展的目的而設(shè)計(jì)的,并具有很明確的文檔說明。

To make this concrete, let’s suppose we have a program that uses a HashSet. To tune the performance of our program, we need to query the HashSet as to how many elements have been added since it was created (not to be confused with its current size, which goes down when an element is removed). To provide this functionality, we write a HashSet variant that keeps count of the number of attempted element insertions and exports an accessor for this count. The HashSet class contains two methods capable of adding elements, add and addAll, so we override both of these methods:

為了使問題更具體一些,讓我們假設(shè)有一個(gè)使用 HashSet 的程序。為了優(yōu)化程序的性能,我們需要查詢 HashSet,以確定自創(chuàng)建以來添加了多少元素(不要與當(dāng)前的大小混淆,當(dāng)元素被刪除時(shí),當(dāng)前的大小會遞減)。為了提供這個(gè)功能,我們編寫了一個(gè) HashSet 變量,它記錄試圖插入的元素?cái)?shù)量,并為這個(gè)計(jì)數(shù)導(dǎo)出一個(gè)訪問。HashSet 類包含兩個(gè)能夠添加元素的方法,add 和 addAll,因此我們覆蓋這兩個(gè)方法:

// Broken - Inappropriate use of inheritance!
public class InstrumentedHashSet<E> extends HashSet<E> {

    // The number of attempted element insertions
    private int addCount = 0;

    public InstrumentedHashSet() {
    }

    public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

This class looks reasonable, but it doesn’t work. Suppose we create an instance and add three elements using the addAll method. Incidentally, note that we create a list using the static factory method List.of, which was added in Java 9; if you’re using an earlier release, use Arrays.asList instead:

這個(gè)類看起來是合理的,但是它不起作用。假設(shè)我們創(chuàng)建了一個(gè)實(shí)例,并使用 addAll 方法添加了三個(gè)元素。順便說一下,我們使用 Java 9 中添加的靜態(tài)工廠方法 List.of 創(chuàng)建了一個(gè)列表;如果你使用的是早期版本,那么使用 Arrays.asList:

InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("Snap", "Crackle", "Pop"));

We would expect the getAddCount method to return three at this point, but it returns six. What went wrong? Internally, HashSet’s addAll method is implemented on top of its add method, although HashSet, quite reasonably,does not document this implementation detail. The addAll method in Instrumented-HashSet added three to addCount and then invoked HashSet’s addAll implementation using super.addAll. This in turn invoked the add method, as overridden in InstrumentedHashSet, once for each element. Each of these three invocations added one more to addCount,for a total increase of six: each element added with the addAll method is double-counted.

我們希望 getAddCount 方法此時(shí)返回 3,但它返回 6。到底是哪里出了錯(cuò)?在內(nèi)部,HashSet 的 addAll 方法是在其 add 方法之上實(shí)現(xiàn)的,盡管 HashSet 相當(dāng)合理地沒有記錄這個(gè)實(shí)現(xiàn)細(xì)節(jié)。InstrumentedHashSet 中的 addAll 方法向 addCount 添加了三個(gè)元素,然后使用 super.addAll 調(diào)用 HashSet 的 addAll 實(shí)現(xiàn)。這反過來調(diào)用 add 方法(在 InstrumentedHashSet 中被覆蓋),每個(gè)元素一次。這三個(gè)調(diào)用中的每一個(gè)都向 addCount 添加了一個(gè)元素,總共增加了 6 個(gè)元素:使用 addAll 方法添加的每個(gè)元素都被重復(fù)計(jì)數(shù)。

We could “fix” the subclass by eliminating its override of the addAll method. While the resulting class would work, it would depend for its proper function on the fact that HashSet’s addAll method is implemented on top of its add method. This “self-use” is an implementation detail, not guaranteed to hold in all implementations of the Java platform and subject to change from release to release. Therefore, the resulting InstrumentedHashSet class would be fragile.

我們可以通過消除 addAll 方法的覆蓋來「修復(fù)」子類。雖然生成的類可以工作,但它的正確功能取決于 HashSet 的 addAll 方法是在 add 方法之上實(shí)現(xiàn)的事實(shí)。這種「自用」是實(shí)現(xiàn)細(xì)節(jié),不能保證在 Java 平臺的所有實(shí)現(xiàn)中都存在,也不能保證在版本之間進(jìn)行更改。因此,結(jié)果得到的 InstrumentedHashSet 類是脆弱的。

It would be slightly better to override the addAll method to iterate over the specified collection, calling the add method once for each element. This would guarantee the correct result whether or not HashSet’s addAll method were implemented atop its add method because HashSet’s addAll implementation would no longer be invoked. This technique, however, does not solve all our problems. It amounts to reimplementing superclass methods that may or may not result in self-use, which is difficult, time-consuming, errorprone,and may reduce performance. Additionally, it isn’t always possible because some methods cannot be implemented without access to private fields inaccessible to the subclass.

覆蓋 addAll 方法以遍歷指定的集合稍微好一些,為每個(gè)元素調(diào)用一次 add 方法。無論 HashSet 的 addAll 方法是否在其 add 方法之上實(shí)現(xiàn),這都將保證正確的結(jié)果,因?yàn)?HashSet 的 addAll 實(shí)現(xiàn)將不再被調(diào)用。然而,這種技術(shù)并不能解決我們所有的問題。它相當(dāng)于重新實(shí)現(xiàn)超類方法,這可能會導(dǎo)致「自用」,也可能不會,這是困難的、耗時(shí)的、容易出錯(cuò)的,并且可能會降低性能。此外,這并不總是可能的,因?yàn)槿绻辉L問子類無法訪問的私有字段,就無法實(shí)現(xiàn)某些方法。

A related cause of fragility in subclasses is that their superclass can acquire new methods in subsequent releases. Suppose a program depends for its security on the fact that all elements inserted into some collection satisfy some predicate.This can be guaranteed by subclassing the collection and overriding each method capable of adding an element to ensure that the predicate is satisfied before adding the element. This works fine until a new method capable of inserting an element is added to the superclass in a subsequent release. Once this happens, it becomes possible to add an “illegal” element merely by invoking the new method, which is not overridden in the subclass. This is not a purely theoretical problem. Several security holes of this nature had to be fixed when Hashtable and Vector were retrofitted to participate in the Collections Framework.

子類脆弱的一個(gè)原因是他們的超類可以在后續(xù)版本中獲得新的方法。假設(shè)一個(gè)程序的安全性取決于插入到某個(gè)集合中的所有元素滿足某個(gè)斷言。這可以通過子類化集合和覆蓋每個(gè)能夠添加元素的方法來確保在添加元素之前滿足斷言。這可以很好地工作,直到在后續(xù)版本中向超類中添加能夠插入元素的新方法。一旦發(fā)生這種情況,只需調(diào)用新方法就可以添加「非法」元素,而新方法在子類中不會被覆蓋。這不是一個(gè)純粹的理論問題。當(dāng) Hashtable 和 Vector 被重新改裝以加入 Collections 框架時(shí),必須修復(fù)幾個(gè)這種性質(zhì)的安全漏洞。

Both of these problems stem from overriding methods. You might think that it is safe to extend a class if you merely add new methods and refrain from overriding existing methods. While this sort of extension is much safer, it is not without risk. If the superclass acquires a new method in a subsequent release and you have the bad luck to have given the subclass a method with the same signature and a different return type, your subclass will no longer compile [JLS, 8.4.8.3]. If you’ve given the subclass a method with the same signature and return type as the new superclass method, then you’re now overriding it, so you’re subject to the problems described earlier. Furthermore, it is doubtful that your method will fulfill the contract of the new superclass method, because that contract had not yet been written when you wrote the subclass method.

這兩個(gè)問題都源于覆蓋方法。你可能認(rèn)為,如果只添加新方法,并且不覆蓋現(xiàn)有方法,那么擴(kuò)展類是安全的。雖然這種擴(kuò)展會更安全,但也不是沒有風(fēng)險(xiǎn)。如果超類在隨后的版本中獲得了一個(gè)新方法,而你不幸給了子類一個(gè)具有相同簽名和不同返回類型的方法,那么你的子類將不再編譯 [JLS, 8.4.8.3]。如果給子類一個(gè)方法,該方法具有與新超類方法相同的簽名和返回類型,那么現(xiàn)在要覆蓋它,因此你要面對前面描述的問題。此外,你的方法是否能夠完成新的超類方法的約定是值得懷疑的,因?yàn)樵谀憔帉懽宇惙椒〞r(shí),該約定還沒有被寫入。

Luckily, there is a way to avoid all of the problems described above. Instead of extending an existing class, give your new class a private field that references an instance of the existing class. This design is called composition because the existing class becomes a component of the new one. Each instance method in the new class invokes the corresponding method on the contained instance of the existing class and returns the results. This is known as forwarding, and the methods in the new class are known as forwarding methods. The resulting class will be rock solid, with no dependencies on the implementation details of the existing class. Even adding new methods to the existing class will have no impact on the new class. To make this concrete, here’s a replacement for InstrumentedHashSet that uses the composition-and-forwarding approach. Note that the implementation is broken into two pieces, the class itself and a reusable forwarding class, which contains all of the forwarding methods and nothing else:

幸運(yùn)的是,有一種方法可以避免上述所有問題。與其擴(kuò)展現(xiàn)有類,不如為新類提供一個(gè)引用現(xiàn)有類實(shí)例的私有字段。這種設(shè)計(jì)稱為復(fù)合,因?yàn)楝F(xiàn)有的類是新類的一個(gè)組件。新類中的每個(gè)實(shí)例方法調(diào)用現(xiàn)有類的包含實(shí)例上的對應(yīng)方法,并返回結(jié)果。這稱為轉(zhuǎn)發(fā),新類中的方法稱為轉(zhuǎn)發(fā)方法。生成的類將非常堅(jiān)固,不依賴于現(xiàn)有類的實(shí)現(xiàn)細(xì)節(jié)。即使向現(xiàn)有類添加新方法,也不會對新類產(chǎn)生影響。為了使其具體化,這里有一個(gè)使用復(fù)合和轉(zhuǎn)發(fā)方法的方法,用以替代 InstrumentedHashSet。注意,實(shí)現(xiàn)被分成兩部分,類本身和一個(gè)可復(fù)用的轉(zhuǎn)發(fā)類,其中包含所有的轉(zhuǎn)發(fā)方法,沒有其他內(nèi)容:

// Wrapper class - uses composition in place of inheritance
public class InstrumentedSet<E> extends ForwardingSet<E> {

    private int addCount = 0;

    public InstrumentedSet(Set<E> s) {
        super(s);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

// Reusable forwarding class
public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s; }
    public void clear() { s.clear(); }
    public boolean contains(Object o) { return s.contains(o); }
    public boolean isEmpty() { return s.isEmpty(); }
    public int size() { return s.size(); }
    public Iterator<E> iterator() { return s.iterator(); }
    public boolean add(E e) { return s.add(e); }
    public boolean remove(Object o) { return s.remove(o); }
    public boolean containsAll(Collection<?> c)
    { return s.containsAll(c); }
    public boolean addAll(Collection<? extends E> c)
    { return s.addAll(c); }
    public boolean removeAll(Collection<?> c)
    { return s.removeAll(c); }
    public boolean retainAll(Collection<?> c)
    { return s.retainAll(c); }
    public Object[] toArray() { return s.toArray(); }
    public <T> T[] toArray(T[] a) { return s.toArray(a); }

    @Override
    public boolean equals(Object o){ return s.equals(o); }

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

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

The design of the InstrumentedSet class is enabled by the existence of the Set interface, which captures the functionality of the HashSet class.Besides being robust, this design is extremely flexible. The InstrumentedSet class implements the Set interface and has a single constructor whose argument is also of type Set. In essence, the class transforms one Set into another, adding the instrumentation functionality. Unlike the inheritance-based approach, which works only for a single concrete class and requires a separate constructor for each supported constructor in the superclass,the wrapper class can be used to instrument any Set implementation and will work in conjunction with any preexisting constructor:

InstrumentedSet 類的設(shè)計(jì)是通過 Set 接口來實(shí)現(xiàn)的,這個(gè)接口可以捕獲 HashSet 類的功能。除了健壯外,這個(gè)設(shè)計(jì)非常靈活。InstrumentedSet 類實(shí)現(xiàn)了 Set 接口,有一個(gè)構(gòu)造函數(shù),它的參數(shù)也是 Set 類型的。實(shí)際上,這個(gè)類可以將任何一個(gè) Set 轉(zhuǎn)換成另一個(gè) Set,并添加 instrumentation 的功能?;诶^承的方法只適用于單個(gè)具體類,并且需要為超類中每個(gè)受支持的構(gòu)造函數(shù)提供單獨(dú)的構(gòu)造函數(shù),與此不同的是,包裝器類可用于儀器任何集合實(shí)現(xiàn),并將與任何現(xiàn)有構(gòu)造函數(shù)一起工作:

Set<Instant> times = new InstrumentedSet<>(new TreeSet<>(cmp));
Set<E> s = new InstrumentedSet<>(new HashSet<>(INIT_CAPACITY));

The InstrumentedSet class can even be used to temporarily instrument a set instance that has already been used without instrumentation:

InstrumentedSet 類甚至還可以用來臨時(shí)配置一個(gè)沒有 instrumentation 功能的 Set 實(shí)例:

static void walk(Set<Dog> dogs) {
InstrumentedSet<Dog> iDogs = new InstrumentedSet<>(dogs);
... // Within this method use iDogs instead of dogs
}

The InstrumentedSet class is known as a wrapper class because each InstrumentedSet instance contains (“wraps”) another Set instance. This is also known as the Decorator pattern [Gamma95] because the InstrumentedSet class “decorates” a set by adding instrumentation. Sometimes the combination of composition and forwarding is loosely referred to as delegation. Technically it’s not delegation unless the wrapper object passes itself to the wrapped object [Lieberman86; Gamma95].

InstrumentedSet 類被稱為包裝類,因?yàn)槊總€(gè) InstrumentedSet 實(shí)例都包含(「包裝」)另一個(gè)集合實(shí)例。這也稱為裝飾者模式 [Gamma95],因?yàn)?InstrumentedSet 類通過添加插裝來「裝飾」一個(gè)集合。有時(shí)復(fù)合和轉(zhuǎn)發(fā)的組合被不當(dāng)?shù)胤Q為委托。嚴(yán)格來說,除非包裝器對象將自身傳遞給包裝對象,否則它不是委托 [Lieberman86; Gamma95]。

The disadvantages of wrapper classes are few. One caveat is that wrapper classes are not suited for use in callback frameworks, wherein objects pass selfreferences to other objects for subsequent invocations (“callbacks”). Because a wrapped object doesn’t know of its wrapper, it passes a reference to itself (this) and callbacks elude the wrapper. This is known as the SELF problem [Lieberman86]. Some people worry about the performance impact of forwarding method invocations or the memory footprint impact of wrapper objects. Neither turn out to have much impact in practice. It’s tedious to write forwarding methods, but you have to write the reusable forwarding class for each interface only once, and forwarding classes may be provided for you. For example, Guava provides forwarding classes for all of the collection interfaces [Guava].

包裝類的缺點(diǎn)很少。一個(gè)需要注意的點(diǎn)是:包裝類不適合在回調(diào)框架中使用,在回調(diào)框架中,對象為后續(xù)調(diào)用(「回調(diào)」)將自定義傳遞給其他對象。因?yàn)榘b對象不知道它的包裝器,所以它傳遞一個(gè)對它自己的引用(this),回調(diào)避開包裝器。這就是所謂的「自用」問題。有些人擔(dān)心轉(zhuǎn)發(fā)方法調(diào)用的性能影響或包裝器對象的內(nèi)存占用影響。這兩種方法在實(shí)踐中都沒有多大影響。編寫轉(zhuǎn)發(fā)方法很麻煩,但是你必須只為每個(gè)接口編寫一次可復(fù)用的轉(zhuǎn)發(fā)類,而且可能會為你提供轉(zhuǎn)發(fā)類。例如,Guava 為所有的集合接口提供了轉(zhuǎn)發(fā)類 [Guava]。

Inheritance is appropriate only in circumstances where the subclass really is a subtype of the superclass. In other words, a class B should extend a class A only if an “is-a” relationship exists between the two classes. If you are tempted to have a class B extend a class A, ask yourself the question: Is every B really an A?If you cannot truthfully answer yes to this question, B should not extend A. If the answer is no, it is often the case that B should contain a private instance of A and expose a different API: A is not an essential part of B, merely a detail of its implementation.

只有在子類確實(shí)是超類的子類型的情況下,繼承才合適。換句話說,只有當(dāng)兩個(gè)類之間存在「is-a」關(guān)系時(shí),類 B 才應(yīng)該擴(kuò)展類 a。如果你想讓 B 類擴(kuò)展 a 類,那就問問自己:每個(gè) B 都是 a 嗎?如果你不能如實(shí)回答是的這個(gè)問題,B 不應(yīng)該擴(kuò)展 a,如果答案是否定的,通常情況下,B 應(yīng)該包含一個(gè)私人的實(shí)例,讓不同的 API:不是 B 的一個(gè)重要組成部分,只是一個(gè)細(xì)節(jié)的實(shí)現(xiàn)。

There are a number of obvious violations of this principle in the Java platform libraries. For example, a stack is not a vector, so Stack should not extend Vector. Similarly, a property list is not a hash table, so Properties should not extend Hashtable. In both cases, composition would have been preferable.

在 Java 庫中有許多明顯違反這一原則的地方。例如,stack 不是 vector,因此 Stack 不應(yīng)該繼承 Vector。類似地,property 列表不是 hash 表,因此 Properties 不應(yīng)該繼承 Hashtable。在這兩種情況下,復(fù)合都是可取的。

If you use inheritance where composition is appropriate, you needlessly expose implementation details. The resulting API ties you to the original implementation, forever limiting the performance of your class. More seriously,by exposing the internals you let clients access them directly. At the very least, it can lead to confusing semantics. For example, if p refers to a Properties instance, then p.getProperty(key) may yield different results from p.get(key): the former method takes defaults into account, while the latter method, which is inherited from Hashtable, does not. Most seriously, the client may be able to corrupt invariants of the subclass by modifying the superclass directly. In the case of Properties, the designers intended that only strings be allowed as keys and values, but direct access to the underlying Hashtable allows this invariant to be violated. Once violated, it is no longer possible to use other parts of the Properties API (load and store). By the time this problem was discovered, it was too late to correct it because clients depended on the use of non-string keys and values.

如果在復(fù)合適用的地方使用了繼承,就會不必要地公開實(shí)現(xiàn)細(xì)節(jié)。生成的 API 將你與原始實(shí)現(xiàn)綁定在一起,永遠(yuǎn)限制了類的性能。更嚴(yán)重的是,通過公開內(nèi)部組件,你可以讓客戶端直接訪問它們。至少,它會導(dǎo)致語義混亂。例如,如果 p 引用了一個(gè) Properties 類的實(shí)例,那么 p.getProperty(key) 可能會產(chǎn)生與 p.get(key) 不同的結(jié)果:前者考慮了默認(rèn)值,而后者(從 Hashtable 繼承而來)則不會。最嚴(yán)重的是,客戶端可以通過直接修改超類來破壞子類的不變量。對于 Properties 類,設(shè)計(jì)者希望只允許字符串作為鍵和值,但是直接訪問底層 Hashtable 允許違反這個(gè)不變性。一旦違反,就不再可能使用 Properties API 的其他部分(加載和存儲)。當(dāng)發(fā)現(xiàn)這個(gè)問題時(shí),已經(jīng)太晚了,無法糾正它,因?yàn)榭蛻舳艘蕾囉诜亲址I和值的使用。

There is one last set of questions you should ask yourself before deciding to use inheritance in place of composition. Does the class that you contemplate extending have any flaws in its API? If so, are you comfortable propagating those flaws into your class’s API? Inheritance propagates any flaws in the superclass’s API, while composition lets you design a new API that hides these flaws.

在決定使用繼承而不是復(fù)合之前,你應(yīng)該問自己最后一組問題。你打算擴(kuò)展的類在其 API 中有任何缺陷嗎?如果是這樣,你是否愿意將這些缺陷傳播到類的 API 中?繼承傳播超類 API 中的任何缺陷,而復(fù)合允許你設(shè)計(jì)一個(gè)新的 API 來隱藏這些缺陷。

To summarize, inheritance is powerful, but it is problematic because it violates encapsulation. It is appropriate only when a genuine subtype relationship exists between the subclass and the superclass. Even then, inheritance may lead to fragility if the subclass is in a different package from the superclass and the superclass is not designed for inheritance. To avoid this fragility, use composition and forwarding instead of inheritance, especially if an appropriate interface to implement a wrapper class exists. Not only are wrapper classes more robust than subclasses, they are also more powerful.

總而言之,繼承是強(qiáng)大的,但是它是有問題的,因?yàn)樗蚱屏朔庋b。只有當(dāng)子類和超類之間存在真正的子類型關(guān)系時(shí)才合適。即使這樣,如果子類與超類不在一個(gè)不同的包中,并且超類不是為繼承而設(shè)計(jì)的,繼承也可能導(dǎo)致程序脆弱。為了避免這種缺陷,應(yīng)使用復(fù)合和轉(zhuǎn)發(fā)而不是繼承,特別是如果存在實(shí)現(xiàn)包裝器類的適當(dāng)接口的話。包裝類不僅比子類更健壯,而且更強(qiáng)大。


Back to contents of the chapter(返回章節(jié)目錄)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi閱讀 7,817評論 0 10
  • 感賞今天上了個(gè)早班,可以早點(diǎn)回家休息。因?yàn)樽蛱鞄团笥汛嗾玖艘惶?,累死了。今天正好下了早班,好好休息啦?感賞今天...
    離不若閱讀 272評論 0 0
  • 從大漠邊陲的田間地頭到大洋彼岸的學(xué)術(shù)殿堂,從理論探索的鍥而不舍到生產(chǎn)實(shí)踐的精益求精,從鶴發(fā)童顏的學(xué)術(shù)前輩到朝氣蓬勃...
    _Dione_閱讀 329評論 0 0
  • 要想實(shí)現(xiàn)網(wǎng)絡(luò)傳輸,需要考慮的問題有哪些? 1.1 如何才能準(zhǔn)確的定位網(wǎng)絡(luò)上的一臺主機(jī)?1.2 如何才能進(jìn)行可靠的、...
    cuteximi_1995閱讀 168評論 0 2
  • 心本不苦,苦是因?yàn)槊允氲锰^。 心本無累,累是因?yàn)榉挪幌碌奶唷?欲望就像手中的沙子,握得越緊,失去的越多。 ...
    清風(fēng)明月馮耀杰閱讀 449評論 0 11

友情鏈接更多精彩內(nèi)容