日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > java >内容正文

java

【Java】一次简单实验经历——社交网络图的简化实现

發(fā)布時(shí)間:2023/12/14 java 23 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【Java】一次简单实验经历——社交网络图的简化实现 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

文章目錄

  • 前言
      • java簡介
      • 引子
      • 代碼資源
  • 1、實(shí)驗(yàn)簡述
    • 1.1 問題簡述
    • 1.2 思路
      • 1.2.1 淺顯的觀點(diǎn)
      • 1.2.2 面臨的選擇
  • 2、設(shè)計(jì)Person類(Version 0.5)
    • 2.1 Person類的開始
      • 2.1.1 字段的定義
      • 2.1.2 構(gòu)造方法
      • 2.1.3 定義對(duì)字段的訪問
    • 2.2 關(guān)于PersonA的一點(diǎn)小修改(Version 0.6)
  • 3 危險(xiǎn)的半成品(Verson 1.0)
      • 3.0.0 引子
    • 3.1 FriendshipGraphA
      • 3.1.1 字段
      • 3.1.2 輔助方法
      • 3.1.3 addVertex
        • **一碗雞湯**
      • 3.1.4 addEdge
    • 3.2 FriendshipGraphB
  • 4 反思與解決(Version 1.1)
    • 4.1 分析出錯(cuò)的原因
      • 4.1.1 所以這只是一個(gè)小失誤
      • 4.1.2 小陳的測(cè)試
    • 4.2 針對(duì)Version 0.5 中出現(xiàn)的問題進(jìn)行修復(fù)
      • 4.2.1 重寫equals方法
      • 4.2.2 重寫hashCode方法
      • 4.2.3 為什么做這樣的重寫?
      • 4.2.4 堅(jiān)決摒棄原來的寫法
    • 4.3 收尾工作
      • 4.3.1 小結(jié)
  • 5 隱藏的錯(cuò)誤(Version1.2)
    • 5.1 一波未平一波又起
    • 5.2 回到addEdge
    • 5.3 問題的根源
      • 5.3.1 為什么會(huì)產(chǎn)生依賴?
        • 5.3.1.1 PersonB
        • 5.3.1.2 PersonA
    • 5.4 權(quán)衡A和B
      • 5.4.1 要發(fā)生拓展了
        • 5.4.1.1拓展
        • 5.4.1.2 再拓展
        • 5.4.1.3 再再再拓展
      • 5.4.2 小結(jié)
  • 6 完成(Verson 1.3)
  • 7 總結(jié)
    • 7.1 Note and improve
      • 7.1.1 方案A
      • 7.1.2 方案B
    • 7.2 通用的經(jīng)驗(yàn)
    • 7.3 后記
  • 參考內(nèi)容


前言

java簡介

Java是一門面向?qū)ο蟮木幊陶Z言,不僅吸收了C++語言的各種優(yōu)點(diǎn),還摒棄了C++里難以理解的多繼承、指針等概念,因此Java語言具有功能強(qiáng)大和簡單易用兩個(gè)特征。Java語言作為靜態(tài)面向?qū)ο缶幊陶Z言的代表,極好地實(shí)現(xiàn)了面向?qū)ο罄碚?#xff0c;允許程序員以優(yōu)雅的思維方式進(jìn)行復(fù)雜的編程。
參考百度百科

引子

這年,我參加了課程軟件設(shè)計(jì),在這門課上,我開始接觸java語言。之前都是用C和C++在寫程序,Java語言一方面摒棄了C++的很多復(fù)雜特性,使得編程更加簡單;另一方面,Java這門完全面向?qū)ο?#xff08;不像C++一樣進(jìn)化不完全)的語言又常常給我們這些初學(xué)者帶來一些疑惑。

當(dāng)所有的代碼都必須使用對(duì)象組織起來的時(shí)候,我們常常困擾于如何將各個(gè)對(duì)象聯(lián)系起來。

  • 同樣的信息(字段),儲(chǔ)存在哪個(gè)對(duì)象里更好?
  • 同樣的功能(方法),交給哪個(gè)對(duì)象實(shí)現(xiàn)更好?

軟件構(gòu)造這門課與之前的算法課不同,它更注重教會(huì)我們關(guān)注代碼的正確性健壯性可擴(kuò)展性、更注重教會(huì)我們把代碼零件組裝起來。在這樣的環(huán)境下,我有機(jī)會(huì)去思考這些平時(shí)忽略過去的問題。在之前的實(shí)驗(yàn)中,我往往都是有了想法就去實(shí)現(xiàn),而不是多方權(quán)衡考量;有的時(shí)候是為了完成作業(yè),而不是寫出“好”的代碼。

在這一系列實(shí)驗(yàn)中,我通過親手實(shí)現(xiàn)腦子里的各種想法,將它們進(jìn)行權(quán)衡比較,好像從中總結(jié)出了一些不太成熟的想法,希望可以和各位交流學(xué)習(xí)一下。對(duì)于我而言,這同樣是階段性總結(jié)。

在這篇文章中,我們以初學(xué)者的身份完成一個(gè)簡單的實(shí)驗(yàn)(后面會(huì)附上實(shí)驗(yàn)描述),在這個(gè)過程中,我們會(huì)寫出具有很多bug的代碼,會(huì)產(chǎn)生一些漏洞百出的設(shè)計(jì),但是我們也將積累一些java編程的經(jīng)驗(yàn),養(yǎng)成一些優(yōu)良習(xí)慣。在完成實(shí)驗(yàn)的過程中,我們不斷自我反思,不斷從兩個(gè)維度評(píng)估寫出來的代碼。

  • 橫向維度,我對(duì)這一實(shí)驗(yàn)提出了兩種實(shí)現(xiàn)方法,在每一部分都將分析它們的差異,和優(yōu)缺點(diǎn)。
  • 縱向維度,這兩種實(shí)現(xiàn)都是在時(shí)間線上迭代開發(fā)完成的。我們將看到,在未來需求未知的情況下,怎樣組織代碼,可以讓程序更好地適應(yīng)變化。

代碼資源

在本實(shí)驗(yàn)中使用到的所有代碼都已經(jīng)共享到github倉庫以供參考,如有需要可以自行下載
(里面有詳盡的spec,和所有測(cè)試用例)
倉庫鏈接在此
然后,我在log.txt文件中也記錄了每個(gè)版本的版本號(hào),代碼下載到本地后,可以在git bash中輸入指令
git reset <版本號(hào)> 切換到不同版本體驗(yàn)


1、實(shí)驗(yàn)簡述

本實(shí)驗(yàn)來自CMU 17-214軟件構(gòu)造課

1.1 問題簡述

  • 總體要求實(shí)現(xiàn)并測(cè)試一個(gè) FriendshipGraph 類,該類表示社交網(wǎng)絡(luò)中的友誼關(guān)系,并且可以計(jì)算圖中兩個(gè)人之間的距離。還需要實(shí)現(xiàn)一個(gè)輔助類 Person。您應(yīng)該將社交網(wǎng)絡(luò)建模為一個(gè)無向圖,其中每個(gè)人都連接到零個(gè)或多個(gè)人,但你的底層圖實(shí)現(xiàn)應(yīng)該是有向的

  • 關(guān)于Person類:你可以假設(shè)每個(gè)人都有一個(gè)唯一的名稱。

  • 關(guān)于FriendshipGraph類:FriendshipGraph類至少需要實(shí)現(xiàn)三個(gè)方法:addVertex(添加節(jié)點(diǎn))、addEdge(添加邊)和getDistance 方法——將兩個(gè)人(作為 Person)作為參數(shù)并返回人之間的最短距離(一個(gè) int),如果兩個(gè)人沒有連接(或者換句話說,沒有任何路徑可以到達(dá)),則返回 -1第一個(gè)人的第二個(gè)人)。

  • 關(guān)于異常輸入:您可以根據(jù)需要處理不正確的輸入(打印到標(biāo)準(zhǔn)輸出/錯(cuò)誤、靜默失敗、崩潰、拋出特殊異常等)

  • 一些tips:您的圖形實(shí)現(xiàn)應(yīng)該具有合理的可擴(kuò)展性。我們將使用數(shù)百或數(shù)千個(gè)頂點(diǎn)和邊測(cè)試您的圖。為您的字段和方法使用適當(dāng)?shù)脑L問修飾符(公共、私有等)。如果一個(gè)字段/方法可以是私有的,它應(yīng)該是私有的。不要使用靜態(tài)字段或方法,除了 main 方法和常量遵循 Java 代碼約定,尤其是命名和注釋。

1.2 思路

1.2.1 淺顯的觀點(diǎn)

單從實(shí)驗(yàn)要求上看,這其實(shí)并不是一個(gè)復(fù)雜的設(shè)計(jì)——畢竟只是一個(gè)有向圖的實(shí)現(xiàn)。

站在圖的角度看,圖中的每一個(gè)節(jié)點(diǎn)都被抽象為一個(gè)Person,而Person與Person之間的“邊”其實(shí)就代表這人與人的“認(rèn)識(shí)”關(guān)系。如果A認(rèn)識(shí)B,那么圖中就應(yīng)當(dāng)有一條從A到B的邊。

站在Person的角度看,在這個(gè)問題中,題目對(duì)Person類做了簡化,現(xiàn)實(shí)中的Person應(yīng)當(dāng)擁有更多的屬性,但是這里僅僅使用唯一的name屬性來描述一個(gè)Person。這其實(shí)隱式要求了,圖中的每個(gè)節(jié)點(diǎn)要具有不同的name,不然沒法從語義上將兩個(gè)節(jié)點(diǎn)分開

1.2.2 面臨的選擇

我們需要抽象Person與Person的認(rèn)識(shí)關(guān)系。這在編程中如何表示呢?我經(jīng)過了一番思考,想出了三種方法:

  • 有一種很貼近現(xiàn)實(shí)的想法。一個(gè)人究竟認(rèn)識(shí)誰,這不應(yīng)該是一個(gè)Person的屬性嗎?就像姓名、年齡、身高一樣,應(yīng)該是Person的一部分,我只要訪問這個(gè)Person就應(yīng)該能夠知道他認(rèn)識(shí)了誰。所以,一個(gè)Person認(rèn)識(shí)的所有其他Person都應(yīng)該儲(chǔ)存在這個(gè)Person內(nèi)部。
  • 從圖出發(fā)想法。Person與Person之間的認(rèn)識(shí)關(guān)系,這不就是圖中的一條條邊嗎?所以,每一條邊<Person, Person>都應(yīng)當(dāng)以某種合適的形式儲(chǔ)存在圖中。
  • 再創(chuàng)建一個(gè)類Relationship。使用Relationship抽象人與人的認(rèn)識(shí)關(guān)系。這樣,圖只用管理一個(gè)一個(gè)節(jié)點(diǎn)(Person)和一條一條邊(Relationship)。在本文中,我們不對(duì)該想法做出實(shí)現(xiàn),有興趣的讀者可以嘗試實(shí)現(xiàn),再與本文展示的實(shí)現(xiàn)進(jìn)行比較

第二種方法和第三種方法的區(qū)別在于,有沒有把邊抽象為一個(gè)類來實(shí)現(xiàn)。在第二種方法中,我們使用java提供的集合類將邊變成一種打包的結(jié)構(gòu),而第三種方法更側(cè)重于把每一條邊變成一個(gè)實(shí)例,我們可以在實(shí)驗(yàn)中感受這兩者的不同。


2、設(shè)計(jì)Person類(Version 0.5)

在Version 0.5 中我們做的事情并不多,只是一些準(zhǔn)備性工作。可以從git提交描述看到,只是
Completed the definition of the ‘naive’ Person class

2.1 Person類的開始

為了解答剛剛提出的問題:怎樣表示Person與Person之間的認(rèn)識(shí)關(guān)系,我在這里實(shí)現(xiàn)了兩個(gè)相似而又不同的Person類:PersonA 和 PersonB。我們可以在后面的編程中看到,Person類的設(shè)計(jì)是如何影響整個(gè)程序的走向的。

2.1.1 字段的定義

首先,我們要定義PersonA和PersonB的字段,它們都擁有相同的字段name,我們將name定義為final,指明一個(gè)人不能更換自己的名字(至少現(xiàn)在不能,估計(jì)以后也不會(huì)有),顯然,要是Person能隨意更改名字,我們的設(shè)計(jì)會(huì)變得很麻煩。

  • PersonA:PersonA實(shí)現(xiàn)了我們的第一個(gè)想法,將一個(gè)人認(rèn)識(shí)的所有人當(dāng)作這個(gè)人的屬性,儲(chǔ)存在類的內(nèi)部。我們可以看到它有兩個(gè)字段-- name 和 knows
// Implemented in PersonA.javapublic class PersonA{/* private field */private final String name;private final Set<PersonA> knows;/* public methods ... */ }
  • PersonB:PersonB則沿用了我們關(guān)于圖形的思考方法,將一個(gè)人認(rèn)識(shí)另一個(gè)人這種聯(lián)系抽象為“邊”,而把關(guān)于邊的操作交給FriendshipGraph類來處理,而PersonB只記錄一個(gè)人的名字。所以PersonB看起來甚至沒有什么內(nèi)容。
// Implemented in PersonB.javapublic class PersonB{/* private field */private final String name;/* public methods ... */ }

2.1.2 構(gòu)造方法

在這個(gè)方面,兩個(gè)Person還是很相近的。你以為構(gòu)造函數(shù)只用接受一個(gè)String類型的參數(shù),然后把它復(fù)制給name就足夠了嗎?

讓我們多思考一會(huì):你希望Person的名字是一堆空格嗎?如果我說“ ”(一個(gè)空格)是一個(gè)Person,而“ ”(三個(gè)空格)是另一個(gè)Person,你肯定會(huì)覺得很荒謬。或者說,你希望“mike”和“ mike ”別認(rèn)為是兩個(gè)不同的Person嗎?顯然,他們是一樣的,只不過一個(gè)名字里有空格,另一個(gè)沒有罷了。

所以在這個(gè)構(gòu)造函數(shù)里,我們不僅需要去除兩端的空格,還要對(duì)“空名字”拋出異常。這里展示PersonB的構(gòu)造方法,PersonA的構(gòu)造方法,只需要為knows指定一個(gè)新的HashSet<>()即可。

// Implemented in PersonB.javapublic class PersonB{/* private field ... *//* public methods *//*** Constructs a {@code PersonA} whose name is <i>nameString</i>.* We will automatically strip the spaces at both ends of the nameString to get a concise name. * * <p><strong>Requires</strong>: nameString is not none.</p> * @exception IllegalArgumentException if nameString is empty* */public PersonB(String nameString) {String temp = nameString.trim();if (temp.equals(""))throw new IllegalArgumentException("None Name");else { // knows = new HashSet<PersonA>(); this line is needed in PersonAname = temp;}} }

2.1.3 定義對(duì)字段的訪問

遵循類定義的原則,我們盡量將字段聲明為private類型。所以對(duì)于這些private類型的變量,我們需要定義一些public接口,以供外部獲取信息。

  • 我們?cè)赑ersonA 和 PersonB 中都定義了 getName方法,用于獲取name字段
public String getName(){return this.name;}
  • 除此之外,在PersonA中,我們還需要定義一些接口,以便外部訪問knows。暫時(shí)能想到的是:1、我們需要有途徑向knows中添加Person;2、我們還希望知道某一個(gè)Person是不是當(dāng)前Person認(rèn)識(shí)的;3、我們還想知道一個(gè)人究竟認(rèn)識(shí)多少個(gè)人。所以我暫時(shí)先定義了這三個(gè)方法。
  • 注意,我們認(rèn)為Person knows himself 在本實(shí)驗(yàn)中使不允許的。原因在于,在本圖中不該產(chǎn)生從自身到自身的邊。
// Implemented in PersonA.javapublic class PersonA{.../*** Determine whether {@code this} knows {@code PersonA } pA.* * @param pA An instance of {@code PersonA}, but not {@code this} PersonA. * @return if pA in {@code this.knows}, return {@code true}; else return {@code false}.* @exception IllegalArgumentException if pA is the same person as {@code this}.* */public boolean isKnows(PersonA pA) {if (pA.getName.equals(name))throw new IllegalArgumentException("Not defined relationship between a person and itself.");return knows.contains(pA);}/*** Return the number of people this person knows. If this* person knows more than {@code Integer.MAX_VALUE} people, returns* {@code Integer.MAX_VALUE}.* * @return the number of people this person knows* */public int knowsNum() { /* Assuming that this method is reliable */return knows.size(); /* As it only calls Set.size() */} /*** Build a new relationship representing {@code this} knows {@code PersonA} pA* * @param pA An instance of {@code PersonA}, but not {@code this} PersonA. * @exception IllegalArgumentException if pA is the same person as {@code this}.* */public void addKnows(PersonA pA) {if (pA.getName.equals(name))throw new IllegalArgumentException("That a person knows himself seems to be confusing.");knows.add(pA);} }

到此為止,我們看似已經(jīng)實(shí)現(xiàn)了兩個(gè)簡單的Person類。因?yàn)楝F(xiàn)在我們腦子里的想法不是很多,只能先完成到這一步,它們雖然簡單,但是好像看上去和合理,好像可以完成我們給它們的任務(wù)。
其實(shí),我們只要稍微做一下測(cè)試就會(huì)發(fā)現(xiàn),PersonA中的Isknows更本不能得出正確的答案,或者說,它僅僅在某些特殊情況下能得出答案,很顯然,這個(gè)設(shè)計(jì)是錯(cuò)誤的。

2.2 關(guān)于PersonA的一點(diǎn)小修改(Version 0.6)

前面說到,PersonA的Isknows方法幾乎不能得出正確的答案。原因是,集合類Set的contains方法其實(shí)會(huì)調(diào)用集合中元素類的equals方法判斷一個(gè)元素是否在集合中。而我們自定義的PersonA、PersonB類,它們的默認(rèn)equals方法是比較兩個(gè)實(shí)例的ID值。實(shí)際上,即使我們使用完全相同的字符串創(chuàng)建兩個(gè)Person,它們的ID也往往不相同。所以這個(gè)調(diào)用contains方法并不能解決問題。

我們想到的一個(gè)很直接的解決方案是改寫isknows方法,如下。

public boolean isKnows(PersonA pA) {if (pA.getName.equals(name))throw new IllegalArgumentException("Not defined relationship between a person and itself.");boolean flags = false;for (PersonA eA : knows) {if (eA.getName().equals(pA.getName()))flags = true;}return flags;}

目前看來,這樣就可以解決問題了。但是,隨著實(shí)驗(yàn)推進(jìn),我們會(huì)發(fā)現(xiàn),這只是權(quán)宜之計(jì),還有一些根本性的問題沒有解決。這樣的設(shè)計(jì)也不利于我們拓展Person類,未來我們會(huì)看到,當(dāng)越來越多問題發(fā)生時(shí),修改這樣的程序是一個(gè)很令人惱火的事情。我們可能會(huì)漸漸抓狂。

還好,咱是良心博主,會(huì)時(shí)不時(shí)停下來檢討自己,重新審視自己的代碼,不至于讓問題爆發(fā)時(shí)變得不可控。

3 危險(xiǎn)的半成品(Verson 1.0)

3.0.0 引子

在上一章,我們簡單的設(shè)計(jì)了類 PersonA 和類PersonB,剛開始的時(shí)候它們看起來很完美,后來,我們做了一些小測(cè)試,發(fā)現(xiàn)PersonA中存在bug。所以我們?cè)?Version 0.6 中修復(fù)了這個(gè)bug,并且對(duì)此感到很高興。接下來,我們將正式開始設(shè)計(jì) FriendshipGraph(A/B)。

3.1 FriendshipGraphA

3.1.1 字段

FriendshipGraphA 使用 PersonA 來描述每個(gè)節(jié)點(diǎn),因?yàn)槲覀儼延涀 罢J(rèn)識(shí)了誰”的任務(wù)交給了PersonA,所以看起來FriendshipGraphA很輕松。它只需要記錄這個(gè)圖中有哪些節(jié)點(diǎn)就好了,就像這樣:

public class FriendshipGraphA {/* Private fields */private final Set<PersonA> vertexes = new HashSet<>();/* public methods ... */ }

3.1.2 輔助方法

在開始設(shè)計(jì)實(shí)驗(yàn)要求的三個(gè)方法前,我們需要兩個(gè)“伙計(jì)”幫忙。這兩個(gè)伙計(jì)一個(gè)能告訴我們圖中有多少個(gè)節(jié)點(diǎn)、一個(gè)能告訴我們圖中有多少條邊。它們是 getVertexNum 和 getEdgeNum.

// Implemented in FriendshipGraphA.java/*** Returns the number of vertexes in this graph. If this* graph contains more than {@code Integer.MAX_VALUE} vertexes, returns* {@code Integer.MAX_VALUE}.* * @return the number of vertexes in the graph.* */public int getVertexNum() { /* Assuming that this method is reliable */return vertexes.size();}/*** Returns the number of edges in this graph. If this graph * contains more than {@code Integer.MAX_VALUE} edges, returns* {@code Integer.MAX_VALUE}.* * @return the number of edges in the graph.* */public int getEdgeNum() { /* Assuming that this method is reliable */int sum = 0, temp = 0;for (PersonA pA : vertexes) {temp = sum + pA.knowsNum();if (temp >= 0) {sum = temp;}else {return Integer.MAX_VALUE;}}return sum;}

我們必須得假定,這兩個(gè)方法是正確的。因?yàn)?#xff0c;后續(xù)我們會(huì)使用這兩個(gè)方法測(cè)試addVertex方法和addEdge方法。我們幾乎沒有更基礎(chǔ)的方法可以證明這兩個(gè)方法的正確性。如果硬要說的話,在debug窗口中數(shù)一數(shù)graph變量中究竟有幾個(gè)節(jié)點(diǎn),幾條邊,比較一下這些數(shù)字是否與getVertexNum 和 getEdgeNum的返回值相同,或許是個(gè)不錯(cuò)的方法。但是,我并不打算這么做。

3.1.3 addVertex

addVertex將一個(gè) PersonA 類型的實(shí)例加入到圖 FriendshipGraphA 中。這時(shí)候,我們想起了關(guān)于的隱式約束,一個(gè)簡單有向圖中不能存在相同節(jié)點(diǎn)。也就是說,在我們把待加入的節(jié)點(diǎn)加入到圖中之前,我們需要檢查這個(gè)節(jié)點(diǎn)在圖中是否存在。

雖然我們儲(chǔ)存節(jié)點(diǎn)容器是Set,set類型可以避免重復(fù)元素的出現(xiàn)。當(dāng)我們嘗試加入重復(fù)元素時(shí),set不會(huì)改變?nèi)萜鲀?nèi)的值,從理論上來講,我們可以不做任何檢查,直接調(diào)用Set.add,它會(huì)自動(dòng)處理重復(fù)節(jié)點(diǎn)的情況。但是,這樣做的壞處是,用戶將無法得知,自己嘗試向圖中加入一個(gè)重復(fù)節(jié)點(diǎn)。

默默無聞的程序不是好程序,當(dāng)用戶輸入已經(jīng)存在于圖中的節(jié)點(diǎn)的時(shí)候,我們希望程序能夠拋出異常。就像這樣:

// Implemented in FriendshipGraphA.java/*** Add a new vertex into the graph.* <p><strong>Requires</strong>: The new vertex must not have the * same name with any existed vertex in the graph.</p>* * @param the new vertex to be added into the graph* @exception IllegalArgumentException if the new vertex has* the same name with some vertex in the graph* */public void addVertex(PersonA pA) {for (PersonA p : vertexes) {if (p.getName().equals(pA.getName()))throw new IllegalArgumentException("Existed vertex");}vertexes.add(pA);}

這里我們使用了笨拙的for循環(huán)來判斷,這是因?yàn)槲覀兿肫鹆嗽?strong>Verson 0.5中遇到的問題,Set.contains方法似乎不起作用。當(dāng)時(shí),我們使用權(quán)宜之計(jì),將contains改成for循環(huán),現(xiàn)在,我們漸漸發(fā)現(xiàn),所有需要判斷一個(gè)Person是否在容器類中的操作,都需要使用這種笨拙的方法。

在后面,當(dāng)我們?nèi)虩o可忍時(shí),會(huì)從根本上解決問題。但是,現(xiàn)在,我們嘗試催眠自己:這不是主要問題,我可以接受。(實(shí)際上,我們經(jīng)常這樣做,當(dāng)寫出一些不美觀的代碼時(shí),我們常常選擇性忽略

或許會(huì)有朋友提出下面這種寫法,因?yàn)樗⒁獾絊et類的add方法是有返回值的。我們是否可以根據(jù)返回值來判斷Person在不在集合中呢?

public void addVertex(PersonA pA) {if (!vertexes.add(pA))throw new IllegalArgumentException("Existed vertex");}

實(shí)際上,add方法判斷一個(gè)元素是否在集合中的程序邏輯與contains沒什么區(qū)別,這種方法同樣不管用。

當(dāng)然,我們已經(jīng)提前寫好了測(cè)試用例,上面的addVertex代碼通過了我們的測(cè)試。

一碗雞湯

接下來,我們將完成實(shí)驗(yàn)規(guī)定的第二項(xiàng)內(nèi)容。現(xiàn)在讓我們停下來想一想,你會(huì)發(fā)現(xiàn),我們做了那么多準(zhǔn)備工作,現(xiàn)在才剛剛開始實(shí)驗(yàn)的第二項(xiàng)內(nèi)容。
實(shí)際上在很多項(xiàng)目中,準(zhǔn)備工作是必要的,沒有人希望自己頂層的業(yè)務(wù)邏輯跑在不可靠的代碼上。只有我們做好了大量基本類、基本方法的準(zhǔn)備,頂層開發(fā)才能變得得心應(yīng)手。
但是,我們后面會(huì)發(fā)現(xiàn),前面的準(zhǔn)備還是不充分的,其實(shí)我們走了一些彎路。如果能在一開始就意識(shí)到代碼中的問題,我們就可以減少很多修改的工作量。
不幸的是,以上的這一段話都是站在“事后諸葛亮”的角度說的,當(dāng)我們作為初學(xué)者接受一項(xiàng)工作時(shí),往往并沒有那么多前瞻性的見解,走些彎路很正常,不過希望大家在一次次腦溢血的經(jīng)歷中吸取教訓(xùn),最終總結(jié)出一套經(jīng)得起考驗(yàn)并且不斷完善的代碼編寫體系。

3.1.4 addEdge

在這一節(jié),我們將完成FriendshipGraphA的addEdge方法,并對(duì)他進(jìn)行測(cè)試。這里就不對(duì)addEdge做過多的說明了,直接上代碼。

// Implemented in FriendshipGraphA.java/*** <p>Add a new edge into the graph, which represents one person knows another.</p>* * <p>Obviously, these two vertex should be already in the graph.* And the edge should have not existed in the graph. </p>* * @param srcA the source of the edge.* @param dstA the destination of the edge.* @exception IllegalArgumentException if srcA or dstA does not exist in the graph* @exception IllegalArgumentException if edge <srcA, dstA> has already existed in the graph* */public void addEdge(PersonA srcA, PersonA dstA){boolean flag = false;for (PersonA p : vertexes) {if (p.getName().equals(srcA.getName())) {flag = true;break;}}if (!flag)throw new IllegalArgumentException("srcA not existed in the graph");flag = false;for (PersonA p : vertexes) {if (p.getName().equals(dstA.getName())) {flag = true;break;}}if (!flag)throw new IllegalArgumentException("dstA not existed in the graph");if (srcA.isKnows(dstA))throw new IllegalArgumentException("Duplicated edge");else srcA.addKnows(dstA);}

其實(shí)這個(gè)方法的邏輯十分簡單,就是將一個(gè)PersonA加入到 srcA.knows 中。但是,為了維護(hù)這個(gè)圖的性質(zhì),我們需要進(jìn)行一些額外的判斷。

我們使用了大量代碼去判斷一個(gè)節(jié)點(diǎn)是否在圖中(使用了18行的篇幅),這使得代碼看起來十分臃腫。顯然,應(yīng)該有更好的寫法,但是,我們又一次催眠了自己,“既然都能完成相應(yīng)的功能,何必去追求簡潔呢?”

緊接著,我們對(duì)addEdge方法進(jìn)行了測(cè)試,測(cè)試通過了,看起來沒有什么問題了,我們可以進(jìn)行下一步開發(fā)了。測(cè)試代碼如下:

// Implemented in FriendshipGraphATest@Testpublic void addEdgeTest() {FriendshipGraphA graphA = new FriendshipGraphA();PersonA p1 = new PersonA("Mike");PersonA p2 = new PersonA("John");PersonA p3 = new PersonA("Kiki");PersonA p4 = new PersonA("Dong");graphA.addVertex(p1);graphA.addVertex(p2);graphA.addVertex(p3);/* Test */assertEquals(0, graphA.getEdgeNum());graphA.addEdge(p3, p1);assertEquals(1, graphA.getEdgeNum());graphA.addEdge(p2, p1);assertEquals(2, graphA.getEdgeNum());graphA.addEdge(p1, p2);assertEquals(3, graphA.getEdgeNum());/* not exist vertex case */expectedEx.expect(IllegalArgumentException.class);expectedEx.expectMessage("srcA not existed in the graph");graphA.addEdge(p4, p2);assertEquals(3, graphA.getEdgeNum());/* duplicated edge case */expectedEx.expect(IllegalArgumentException.class);expectedEx.expectMessage("Duplicated edge");graphA.addEdge(p1, p2);assertEquals(3, graphA.getEdgeNum());}

3.2 FriendshipGraphB

有了 FriendshipGraphA 的基礎(chǔ),我們很快就完成了 相關(guān)代碼的實(shí)現(xiàn)。這兩個(gè)類只有兩個(gè)方法不太一樣:getEdgeNum 和 addEdge。
除此之外,兩者幾乎一樣。下面我們貼出FriendshipGraphB中g(shù)etEdgeNum和addEdge的實(shí)現(xiàn)。

// Inplemented in FriendshipGraphB.javapublic int getEdgeNum() {int sum = 0, temp = 0;for (PersonB pB : vertexes) {temp = sum + edges.get(pB).size(); /* this line is differented with that in FriendshipGraphA.java */if (temp >= 0)sum = temp;else return Integer.MAX_VALUE;}return sum;}public void addEdge(PersonB srcB, PersonB dstB) {/* verbose part ... */if (!edges.containsKey(srcB)) { // 如果字典中不包含鍵值 srcSet<PersonB> knows = new HashSet<>(); // 新建一項(xiàng)加入到字典中knows.add(dstB);edges.put(srcB, knows);}else { // 否則if (edges.get(srcB).contains(dstB)) // 先檢查重邊是否存在throw new IllegalArgumentException("There must not be double edges in graph");else // 如果不存在重邊則加入此邊edges.get(srcB).add(dstB);}}

有了FriendshipGraphA測(cè)試成功的經(jīng)歷,我們信誓旦旦地對(duì)FriendshipGraphB做了相同的測(cè)試。
然而,問題出現(xiàn)了。

測(cè)試addEdge方法的方法拋出了異常,原來是測(cè)試函數(shù)中調(diào)用了getEdgeNum方法,而該方法在下述位置產(chǎn)生了錯(cuò)誤。

現(xiàn)在,壓力來到了程序員這邊。


4 反思與解決(Version 1.1)

在上一章,我們完成了FriendshipGraph(A/B)類的半成品。我們依照設(shè)計(jì)FriendshipGraphA的經(jīng)驗(yàn)完成了FriendshipGraphB的設(shè)計(jì)。但是卻在測(cè)試階段出現(xiàn)了問題。在本章中,我們將深入理解產(chǎn)生這些問題的原因,并且提出一些隱藏的危險(xiǎn)情況(這就是我們說Verson 1 是危險(xiǎn)的半成品的原因)

4.1 分析出錯(cuò)的原因

4.1.1 所以這只是一個(gè)小失誤

錯(cuò)誤提示告訴我們:

the return value of "java.util.Map.get(Object)" is null.

亦即,get方法返回了空值。也就是說,當(dāng)我們遍歷Map的所有keys,利用這些keys去取Map中的值時(shí),卻得到了空。也就是說,字典中這個(gè)鍵下面不包含任何元素。稍微往前翻看一下我們的代碼,我們就會(huì)發(fā)現(xiàn),當(dāng)我們把一個(gè)Vertex加入到圖中的時(shí)候,我們并沒有在字典Edge中添加任何有關(guān)這個(gè)節(jié)點(diǎn)的信息,第一次添加來自于第一次調(diào)用addEdge,且第一個(gè)參數(shù)為該節(jié)點(diǎn)時(shí)。

所以,我們或許要對(duì)addVertex方法做一下修改:

// Implemented in FriendshipGraphB.javapublic void addVertex(PersonB pB) {for (PersonB p : vertexes) {if (p.getName().equals(pB.getName()))throw new IllegalArgumentException("Existed vertex");}vertexes.add(pB);edges.put(pB, new HashSet<>()); /* A new line added */}

到此為止,你長吁一口氣。但是為了保險(xiǎn)起見,你把自己的代碼拿給細(xì)心的好友小陳檢查,小陳通讀了你的每一個(gè)類、每一個(gè)測(cè)試用例,就在連小陳都要覺得無懈可擊的時(shí)候。他突然靈光一動(dòng),在你的測(cè)試代碼中留下了這么幾行字。

4.1.2 小陳的測(cè)試

// Implemented in FriendshipGraphB.java@Testpublic void addEdgeTest(){/* ... context ... */PersonB mike = new PersonB("Mike");PersonB mike1 = new PersonB("Mike");PersonB bob = new PersonB("Bob");/* Person named Mike has already been added into the graph */graphB.addVertex(bob);graphB.addEdge(mike, bob);graphB.addEdge(mike1, bob); /* this operation should throw an exception, but not *//* ... context ... */}

這段測(cè)試代碼不會(huì)產(chǎn)生任何錯(cuò)誤,它可以完美的通過。但是,你已經(jīng)冷靜不下來了。因?yàn)?#xff0c;最后一行代碼應(yīng)該出錯(cuò)的呀。從語義上看,mike 和 mike1完全指的是同一個(gè)人。當(dāng)我們嘗試調(diào)用

graphB.addEdge(mike1, bob);

時(shí),理應(yīng)當(dāng)觸發(fā)IllegalArgumentException(“Duplicated edge”)異常。但是小陳設(shè)計(jì)的測(cè)試用例卻繞開了代碼內(nèi)部對(duì)重邊的檢查。

重新審視我們的代碼,我們很快就發(fā)現(xiàn),自己又犯了與Version 0.5 一樣的錯(cuò)誤。因?yàn)槲覀兪褂昧薓ap的containsKey方法,而這個(gè)方法并沒有辦法將擁有相同name字段的不同Person實(shí)例識(shí)別為同一個(gè)人。

// Snippet from addEdge in FriendshipGraphB.java

到這里,相信你一定已經(jīng)意識(shí)到了問題的嚴(yán)重性。其實(shí),在Version 0.5 的時(shí)候,咱們就已經(jīng)發(fā)現(xiàn)了這個(gè)問題,但是,我們不斷使用所謂的“權(quán)宜之計(jì)”,最后,當(dāng)我們意識(shí)到這個(gè)問題可能會(huì)使編程變得十分艱難的時(shí)候,已經(jīng)在錯(cuò)誤的道路上走了很遠(yuǎn)了。

4.2 針對(duì)Version 0.5 中出現(xiàn)的問題進(jìn)行修復(fù)

4.2.1 重寫equals方法

想要使用Set,Map等集合類的contains等方法。我們需要重寫Person類的equals方法。因?yàn)榘⊿et在內(nèi)的容器類型并沒辦法知道它們?nèi)菁{了什么類(今天是Person,明天可能是Graph),所以這些方法內(nèi)部都會(huì)調(diào)用內(nèi)容類的equals方法來判斷其中內(nèi)容是否相等。

而默認(rèn)equals方法的行為是比較兩個(gè)實(shí)例是否具有相同ID值。這是java特性,所有類的實(shí)例通過唯一的ID區(qū)分,我們可以在debug模式下觀察變量得知ID值。所以,我們需要覆蓋equals方法的行為,將其變成比較兩個(gè)實(shí)例的name字段值。如下:

// Implemented both in PersonA.java and PersonB.java/*** <p>Compares this string to the specified object. * The result is {@code true} if and only if the argument is not null * and is a {@code PersonA} object that has the same name as this object. </p>* * @param obj an Object The object to compare this {@code PersonA} against* @return {@code true} if the given object represents a {@code PersonA} * equivalent to this person, {@code false} otherwise.* */@Overridepublic boolean equals(Object obj){if (!(obj instanceof PersonA)) /* PersonB in PersonB.java */return false;if (obj == this)return true;return ((PersonA)obj).getName().equals(this.name); /* convert to PersonB in PersonB.java */}

這里我們遞歸調(diào)用了String類的equals方法,這個(gè)方法會(huì)比較兩個(gè)String實(shí)例中儲(chǔ)存的字符串是否相等,這與我們的目標(biāo)是一致的。

為了驗(yàn)證重寫的equals方法確實(shí)管用,我們需要對(duì)它進(jìn)行測(cè)試,這很重要!!!任何方法在投入使用之前一定要經(jīng)過嚴(yán)格的測(cè)試,保證它符合spec中規(guī)定的行為。這里為了不占用過多篇幅,就不展示測(cè)試代碼了,git倉庫都可以獲得。請(qǐng)相信筆者已經(jīng)對(duì)equals方法做了詳盡的測(cè)試:)

所謂spec,可以通俗理解為方法前的一大段注釋。注釋中規(guī)定了方法使用的前提條件和在某些情況下的行為。而對(duì)于spec中未定義的情況,方法的行為是未知的。

4.2.2 重寫hashCode方法

光有equals方法還不夠,我們現(xiàn)在還不能使用contains方法。
在源代碼中也附上了useContainsTest代碼,感興趣的讀者可以把PersonA、PersonB內(nèi)重寫的hashCode方法注釋掉,這時(shí)候useContainsTest測(cè)試是無法通過的。

// Implemented in PersonATest.java/*** Test contains of Set* * <p>Test whether we can use the {@code contains} method of the {@code Set} * to determine whether an instance of {@code PersonA} is in the set, and the* {@code containsKey} method of the {@code HashMap} to determine whether an * instance of {@code PersonA} is in the key set. </p>* */@Testpublic void useContainsTest() {PersonA mike = new PersonA("Mike");PersonA mike2 = new PersonA("Mike");/* test contains method */Set<PersonA> group = new HashSet<>();group.add(mike);assertEquals(true, group.contains(mike2));/* test containsKey */Map<PersonA, String> dict = new HashMap<>();dict.put(mike, "Mike");assertEquals(true, dict.containsKey(mike2));}

這還需要重寫hashCode方法,如下:

// Implemented in PersonA.java and PersonB.java@Overridepublic int hashCode() {return name.length() + (int)(name.charAt(0));}

這里沒有為hashCode編寫spec是因?yàn)槲覀儭袄^承”了Object類對(duì)hashCode的spec。

4.2.3 為什么做這樣的重寫?

這根我們使用了HashSet和HashMap有關(guān),這兩個(gè)容器類在內(nèi)部都通過哈希表提高查找效率,同樣,它們無法預(yù)測(cè)儲(chǔ)存元素的類型,所以都會(huì)調(diào)用元素類的hashCode方法產(chǎn)生哈希值。
我們調(diào)用contains(arg)和containsKey(arg)方法執(zhí)行的內(nèi)部邏輯是這樣的:

  • 根據(jù)arg的哈希值找到相應(yīng)的“桶”。
  • 在桶中逐個(gè)選出元素調(diào)用equals方法,比較與arg是否相同。

而默認(rèn)hashCode方法可能無法保證為每個(gè)同名的Person實(shí)例產(chǎn)生相同的哈希值,所以我們需要修改hashCode的行為,使得它在任何情況下為同名的Person實(shí)例產(chǎn)生相同的哈希值

當(dāng)我們完成了這兩個(gè)方法的重寫,就可以通過useContainsTest測(cè)試了。

這一部分我使用的筆力不多,覺得筆者在這一部分沒有講清楚的朋友,可以參考下面這兩篇文章。其中第一篇文章也引用了第二篇文章。俺也是從這里學(xué)習(xí)到的。
Java中Set的contains()方法
equals()與hashCode()方法協(xié)作約定

4.2.4 堅(jiān)決摒棄原來的寫法

前面我們從實(shí)踐的角度體驗(yàn)了之前的寫法有多么糟糕,現(xiàn)在我們需要從理論上摒棄這種寫法,雖然重寫hashCode不是必要的,但是可能我們需要把重寫equals放進(jìn)我們的編程法則中。

  • 從上面的例子來看,如果我們不重寫equals方法,我們極易寫出*“屎山代碼”*,從美學(xué)角度十分令人不快
  • 降低耦合度的角度。不提供equals接口會(huì)大大提高代碼耦合度。所有要使用到Person的其他class,如果需要判斷兩個(gè)Person實(shí)例是否相等,那么這個(gè)類就必須要知道Person內(nèi)部有name字段,它要知道必須通過這個(gè)字段來判斷兩個(gè)Perso實(shí)例是否相等。這與我們的編程原則相悖。一個(gè)好的設(shè)計(jì)應(yīng)該允許各個(gè)部分在盡可能少知道其他部分的情況下使用其功能
  • 提高可拓展性的角度。我們想要對(duì)不重寫equals的代碼做修改將變得十分困難。考慮這樣一種情況,你關(guān)于Person類和FriendshipGraph類的設(shè)計(jì)得到了導(dǎo)師的認(rèn)可,現(xiàn)在導(dǎo)師將一個(gè)描述小區(qū)人際關(guān)系的項(xiàng)目交給你。這個(gè)時(shí)候,你需要為Person類添加很多字段用于描述一個(gè)住戶,而且你發(fā)現(xiàn),一個(gè)小區(qū)中有許多同名住戶,這個(gè)時(shí)候使用name字段來判斷兩個(gè)住戶是否為同一住戶已經(jīng)不合適了。可能住址更管用,這就以為著,在所有判斷兩個(gè)住戶是否為同一住戶的時(shí)候,你需要把name字段換成 住址+名字——這是一個(gè)所有程序員都不愿意嘗試的大工程。

4.3 收尾工作

既然我們已經(jīng)重寫了equals方法和hashCode方法,我們就可自由地使用之前一直不敢使用的contains方法和containsKey方法了。接下來,讓我們把相關(guān)替換做完,然后,將版本升級(jí)到Version 1.1吧!

4.3.1 小結(jié)

很多時(shí)候重寫equals方法都是一個(gè)必須的選擇,尤其是你的代碼中有大量比較運(yùn)算的時(shí)候,重寫equals方法可以顯著降低耦合度、也能夠增加程序面對(duì)不同情況的應(yīng)變力。

重寫hashCode卻不是必須的。它往往和我們選擇的數(shù)據(jù)類型有關(guān)。比如在此例中,我們使用了HashSet容器,所以有這方面的需要。但是如果我們使用了類似ArrayList這樣內(nèi)部不使用哈希表實(shí)現(xiàn)的容器類,可能就沒有重寫hashCode的需要了。

但是,如果你選擇重寫hashCode,就意味著你設(shè)計(jì)的類可以在適配java大多數(shù)集合類。如果你可以保證自己的類是immutable的,它就可以作為 Map 的鍵值來使用。


5 隱藏的錯(cuò)誤(Version1.2)

5.1 一波未平一波又起

一個(gè)好習(xí)慣
每次做完修改后,都去跑一下測(cè)試用例。

做完了對(duì)兩個(gè)方法的重寫后,我們也更新了代碼的其他部分,現(xiàn)在,程序看起來十分干凈簡潔。我們運(yùn)行測(cè)試代碼,原來的測(cè)試依然能夠通過,而且在addEdgeTest中小陳給出的測(cè)試用例也會(huì)拋出異常了。說明,現(xiàn)在我們可以檢測(cè)到不同對(duì)象實(shí)例同名的情況了。

再然后,我想著,把小陳的測(cè)試代碼放到FriendshipGraphA中進(jìn)行測(cè)試,這一測(cè),果然出問題了。

有同樣問題的代碼也沒能拋出異常。(這一部分已經(jīng)更新在Version 1.1 中,讀者可以嘗試。結(jié)果為,使用addEdge加入重邊,卻不拋出異常。)

5.2 回到addEdge

// Implemented in FriendshipGraphA.javapublic void addEdge(PersonA srcA, PersonA dstA){if (!vertexes.contains(srcA))throw new IllegalArgumentException("srcA not existed in the graph");if (!vertexes.contains(dstA))throw new IllegalArgumentException("dstA not existed in the graph");if (srcA.isKnows(dstA)) // Check if duplicate edges existthrow new IllegalArgumentException("Duplicated edge");else srcA.addKnows(dstA);}

當(dāng)我們帶著懷疑的眼光重新審視這一段代碼時(shí),我們就會(huì)發(fā)現(xiàn),我們自然而然地犯了一個(gè)不易察覺的錯(cuò)誤。
在第9行到第12行,我們默認(rèn)地把srcA當(dāng)作是graph中的節(jié)點(diǎn)了。

就如同下面這張圖展示的那樣,Vertexes集合中儲(chǔ)存了一系列PersonA對(duì)象的“指針”,其中一個(gè)指向了名為Mike的對(duì)象實(shí)例。然后我們創(chuàng)建了一個(gè)新的PersonA實(shí)例也命名為Mike。
現(xiàn)在讓我們看看執(zhí)行addEdge操作后各個(gè)實(shí)例之間的關(guān)系。這個(gè)時(shí)候聰明的你肯定已經(jīng)發(fā)現(xiàn)問題了。
srcA指向的Mike實(shí)例不一定是Vertexes中儲(chǔ)存的那個(gè)Mike實(shí)例。我們把Bob加入到了一個(gè)Mike實(shí)例的knows集合中,但是這個(gè)Mike實(shí)例并不是圖的一部分。我們的操作并沒有對(duì)圖完成修改。

而我們的代碼之所以會(huì)犯錯(cuò),是因?yàn)橄旅孢@種使用了匿名變量的用法是十分普遍的。幾乎在所有情況下,匿名變量都不在圖中,那么我們的addEdge方法就會(huì)變得十分危險(xiǎn)。

graph.addEdge(new PersonA("Mike"), new PersonA("Bob"));

為了解決這個(gè)問題,我們或許需要把a(bǔ)ddEdge方法更改為下面這種版本

// Implemented in FriendshipGraphA.javapublic void addEdge(PersonA srcA, PersonA dstA){if (!vertexes.contains(dstA))throw new IllegalArgumentException("dstA not existed in the graph");PersonA src = null; /* Find the certain instance stored in Vertexes */for (PersonA item : vertexes)if (item.equals(srcA))src = item;if (src == null) /* NotFound throw exception */throw new IllegalArgumentException("srcA not existed in the graph");if (src.isKnows(dstA)) /* Check if duplicate edges exist */throw new IllegalArgumentException("Duplicated edge");else src.addKnows(dstA);}

5.3 問題的根源

這樣的bug雖然很容易解決,但有時(shí)候很難被察覺。我們希望盡力避免寫出這樣的代碼,而問題的根源出自Person類的設(shè)計(jì)——因?yàn)镻erson類掌握了太多信息,導(dǎo)致,Graph不得不“請(qǐng)求”某一個(gè)特定的Person來解決問題。比如在此例中,認(rèn)識(shí)的人一定要加入到特定的那個(gè)在圖中的"Mike"中才可以。

5.3.1 為什么會(huì)產(chǎn)生依賴?

這一節(jié)的內(nèi)容充斥著個(gè)人見解,各位讀者可以揚(yáng)棄地采納

從直觀上,我們很容易接受這樣一個(gè)觀點(diǎn):一個(gè)類掌握的信息越多,那么它的每一個(gè)實(shí)例就越具有唯一性,被替換的難度就越大。

5.3.1.1 PersonB

對(duì)于PersonB類,我們可以創(chuàng)建很多實(shí)例,只要它們都名為Mike,那么在程序的各個(gè)角落,這些實(shí)例的產(chǎn)生的效果都是等價(jià)的。因?yàn)槊恳粋€(gè)實(shí)例包含的內(nèi)容很少,很容易被復(fù)制,也很容易互相替換。

5.3.1.2 PersonA

而對(duì)于PersonA類,如果我們想保持每一個(gè)Mike實(shí)例的等價(jià)性,除了維護(hù)相同的名字以外,我們還要保證它們的knows字段是相同的。這會(huì)給我們帶來很多困擾,各種保持一致性的方法都好似不盡如人意:

  • 每個(gè)Mike實(shí)例將自身在knows字段上的修改同步到其他Mike上,這意味著每個(gè)Mike都要“知道”其他Mike。在項(xiàng)目的任何時(shí)候,只要產(chǎn)生了一個(gè)PersonA實(shí)例,這個(gè)實(shí)例就要和現(xiàn)存的所有PersonA實(shí)例想比較,找到一個(gè)同名的實(shí)例后,它就要同步該實(shí)例的其他屬性。同時(shí),修改任何一個(gè)Mike都要保證其他Mike收到相同修正。這是不現(xiàn)實(shí)的!!!
  • 所有Mike實(shí)例的knows共同指向一個(gè)Set。既然它們共享了knows,那么就一樣了吧。但是這種做法的安全性極低。因?yàn)槿魏我粋€(gè)Mike實(shí)例都有權(quán)利修改公有信息,只要利用其中一個(gè)Mike實(shí)例就可以對(duì)全局造成不可恢復(fù)的信息破壞。

既然保持每個(gè)PersonA同名實(shí)例的一致性那么困難(我們可能根本不會(huì)選擇去做這樣的事情),這就意味著總有一個(gè)實(shí)例是特殊的,它記錄這其他同名實(shí)例內(nèi)不含有的內(nèi)容。就像在本例中提到的那樣,一旦我們想要為Mike添加一位認(rèn)識(shí)的人,我們就一定要找到圖中,vertex集合內(nèi)指向的那個(gè)Mike實(shí)例。

5.4 權(quán)衡A和B

在前面的內(nèi)容中,我們一手實(shí)現(xiàn) A-方案,一手實(shí)現(xiàn) B-方案。現(xiàn)在,或許是時(shí)候做一個(gè)小結(jié)了。你更喜歡哪種設(shè)計(jì)呢?

  • 從代碼量上看,二者并沒有十分明顯的區(qū)別。并沒有說,某一種設(shè)計(jì)可以簡化實(shí)現(xiàn),
  • 方案A中,Person類既負(fù)責(zé)保存Person的自身屬性,又負(fù)責(zé)記錄它和其他Person的關(guān)系。其實(shí),在Person類中,我們就已經(jīng)實(shí)現(xiàn)了圖的大部分邏輯,Graph只是將Person做了一個(gè)打包封裝,從而對(duì)外界呈現(xiàn)出圖的狀態(tài)。兩者其實(shí)已經(jīng)糅合在一起了。比如說,添加邊的時(shí)候,Graph只能完成其中的一部分,還有一部分要在Person中去完成。
  • 方案B中,Person類只起到儲(chǔ)存信息的作用,Person都不關(guān)心外部世界,它不完成任何業(yè)務(wù)邏輯;所有關(guān)于圖的操作都在Graph中實(shí)現(xiàn)。此時(shí),Person和Graph的關(guān)系是區(qū)分得比較開的,業(yè)務(wù)邏輯是分離的。

我們可能更建議采用方案B。因?yàn)?#xff0c;它的耦合度比較低。

耦合度就是各個(gè)模塊之間的聯(lián)系程度,耦合度越高,程序就越會(huì)牽一發(fā)而動(dòng)全身。

較高的耦合度帶來的副作用往往不體現(xiàn)在當(dāng)下,而是體現(xiàn)在未來。在本項(xiàng)目中,無論是實(shí)現(xiàn)方案A還是實(shí)現(xiàn)方案B,都沒有給我們帶來太多的困難。但是如果要考慮到程序?qū)淼耐卣股?jí),二者的區(qū)別就產(chǎn)生了。

5.4.1 要發(fā)生拓展了

5.4.1.1拓展

考慮這樣一種情況,如果我們想把社交網(wǎng)絡(luò)拓展成帶權(quán)有向圖——這很好理解,權(quán)重可以表示兩人的親密程度,同樣是認(rèn)識(shí)的關(guān)系,親密等級(jí)可能不一樣。

在方案B中,我們可以保持Person類不動(dòng),在另外抽象出一個(gè)Relationship類,用來表示一個(gè)人認(rèn)識(shí)另一人的關(guān)系,然后Graph類復(fù)雜統(tǒng)籌管理Person和Relationship,我們只要把Person和Relationship都設(shè)計(jì)為immutable,這樣的結(jié)構(gòu)是很好實(shí)現(xiàn)的。此時(shí)Graph類中的edge就不再是一個(gè)Map<Person, Set< Person >>,而是Set< Relationship>(或者其他結(jié)構(gòu),比如List等等)。

其實(shí)這里就是開頭提到的第三種實(shí)現(xiàn)方法。

// A possibility derived from option Apublic class Graph{private final Set<Person> vertices;private final Set<Relationship> edges;... }public class Person{private final String name;... }public class Relationship{private final Person src;private final Person dst;private int weight;... }

在方案A中,我們發(fā)現(xiàn),Person類記錄了兩個(gè)Person之間的聯(lián)系,所以勢(shì)必要在Person中做出修改,而且原來的記錄邊的數(shù)據(jù)結(jié)構(gòu)可能已經(jīng)力不從心了,或許我們需要使用Map記錄邊的終點(diǎn)和權(quán)重,所以Person需要提供新的對(duì)外接口。而在Graph中,我們也要修改方法的邏輯實(shí)現(xiàn)一適配Person中的變化。

// A possibility derived from option B public class Graph{private final Set<Person> vertices;... }public class Person{private final String name;// key is the dstination of edge, the value is the weightprivate final Map<String, int> knows;... }

5.4.1.2 再拓展

現(xiàn)在我們要把這個(gè)圖放到更廣義的用途上,比如用它圖來解決網(wǎng)絡(luò)流問題,所以每個(gè)邊上既要記錄最大流量,又要記錄已經(jīng)使用的流量,可能還要記錄剩余流量。

在方案B的基礎(chǔ)上,我們可以做出如下修改:

// A possibility derived from option Bpublic class Graph{private final Set<Vertex> vertices;private final Set<Edge> edges;... }public class Vertex{private final String label;... }public class Relationship{private final Vertex src;private final Vertex dst;private int capability;private int used;private int margin;... }

而在方案A的基礎(chǔ)上,我們可能需要把圖變成這個(gè)樣子???

// A possibility derived from option Apublic class Graph{private final Set<Vertex> vertices;... }public class Vertex{private final String label;private final Set<ClassName> knows;... }// I just don't know how to name this class, given how werid it looks public class ClassName {private final String dst;private int capability;private int used;private int margin;... }

5.4.1.3 再再再拓展

5.4.2 小結(jié)

隨著我們不斷增加圖中的信息,我們會(huì)發(fā)現(xiàn),在方案A中,Person所占的比重越來越大,以至于到最后,所有的信息儲(chǔ)存和業(yè)務(wù)邏輯都在Person中實(shí)現(xiàn),而Graph只是套在外面的一個(gè)外殼,我們只是通過這樣一個(gè)外殼來保持外界使用這個(gè)圖。當(dāng)我們想在圖中添加一條新的特性的時(shí)候,我們很像把它放在Graph內(nèi)部實(shí)現(xiàn),但是又不得不牽扯到對(duì)Person的修改。因?yàn)镻erson占有了很多信息,所以Graph對(duì)每個(gè)Person都十分依賴,業(yè)務(wù)邏輯十分混亂,就像下圖展示的關(guān)系一樣。

在方案B中則不一樣,因?yàn)楦鱾€(gè)類的分工很明確,Vertex和Edge只是作為信息的載體,主要的邏輯都在Graph中實(shí)現(xiàn)。當(dāng)我要拓展Edge的功能時(shí),就修改Edge的表達(dá);需要拓展Vertex的功能時(shí),就修改Vertex的表達(dá);需要增加圖的限制時(shí),就在Graph中添加相應(yīng)的代碼。而且每種改動(dòng)對(duì)其他類的影響是相對(duì)比較少的。


6 完成(Verson 1.3)

現(xiàn)在我們需要完成最后的一個(gè)方法getDistance。這里也沒有什么好說的,直接看實(shí)現(xiàn)。之前使用PersonA和GrpahA的時(shí)候并無過多不適,但是如今一寫這個(gè)求單元最短路的方法,就覺得束手束腳。
首先,如果是單純寫一個(gè)單元最短路的算法是用不到這么多篇幅的。

  • 但是我們需要對(duì)一些特殊的輸入做出特殊反應(yīng)
  • 而且還是 5.2 節(jié)提到的那個(gè)問題,我們一定要找到那個(gè)特定的PersonA實(shí)例,只有在那個(gè)實(shí)例中才儲(chǔ)存了他認(rèn)識(shí)的人。這個(gè)特性有時(shí)候會(huì)令人抓狂的。
/*** Calculate the length of the shortest path from node src to node dst* * @param srcA source of the path* @param dstA destination of the path* @return length of the path if existed, 0 if srcA == dstA, else -1.* @return IllegalArgument if srcA or srcB is null or not existed in the graph* */public int getDistance(PersonA srcA, PersonA dstA) {if (srcA == null || dstA == null)throw new IllegalArgumentException("Null Person");if (!vertexes.contains(srcA))throw new IllegalArgumentException("'"+srcA.getName()+"' not in the graph");if (!vertexes.contains(dstA))throw new IllegalArgumentException("'"+dstA.getName()+"' not in the graph");if (srcA.equals(dstA)) /* when src == dst return 0 */return 0;int dis = 0;Set<PersonA> unvisited = new HashSet<>(vertexes); /* unvisited nodes */Set<PersonA> preAs = new HashSet<>();Set<PersonA> nowAs = new HashSet<>();unvisited.remove(srcA);for (PersonA tempA : vertexes) {if (tempA.equals(srcA)) {preAs.add(tempA);break;}}boolean find = false; // not find dstAFINDER:while (!preAs.isEmpty()) { // while we have new vertexes to visit++dis;for (PersonA item : preAs) { // start from each vertexIterator<PersonA> ite = unvisited.iterator();while(ite.hasNext()) {// traverse unvisited to find a vertex can be reached from itemPersonA unv = ite.next();if (item.isKnows(unv)) {ite.remove();nowAs.add(unv);if (unv.equals(dstA)) {find = true;break FINDER;}}}}preAs.clear();preAs.addAll(nowAs);nowAs.clear();}if (!find)return -1;return dis; }

相形之下,方案B的實(shí)現(xiàn)就會(huì)稍微簡潔一點(diǎn):

public int getDistance(PersonB srcB, PersonB dstB) {if (srcB == null || dstB == null)throw new IllegalArgumentException("Null Person");if (!vertexes.contains(srcB))throw new IllegalArgumentException("'"+srcB.getName()+"' not in the graph");if (!vertexes.contains(dstB))throw new IllegalArgumentException("'"+dstB.getName()+"' not in the graph");if (srcB.equals(dstB)) /* when src == dst return 0 */return 0;int dis = 0;Set<PersonB> unvisited = new HashSet<>(vertexes); /* unvisited nodes */Set<PersonB> preAs = new HashSet<>();Set<PersonB> nowAs = new HashSet<>();unvisited.remove(srcB);preAs.add(srcB);boolean find = false; // not find dstAFINDER:while (!preAs.isEmpty()) { // while we have new vertexes to visit++dis;for (PersonB item : preAs) {for (PersonB dst : edges.get(item)) {if (unvisited.remove(dst)) {nowAs.add(dst);if (dst.equals(dstB)) {find = true;break FINDER;}}}}preAs.clear();preAs.addAll(nowAs);nowAs.clear();}if (!find)return -1;return dis; }

到了這里,實(shí)驗(yàn)已經(jīng)接近尾聲了。每當(dāng)?shù)搅诉@個(gè)時(shí)候,一方面我們長吁一口氣,放下了心中的石頭;另一方面,我們又要對(duì)這次實(shí)驗(yàn)做出總結(jié),準(zhǔn)備收拾行囊 再出發(fā)。

7 總結(jié)

7.1 Note and improve

就這個(gè)實(shí)驗(yàn)而言談一談我們有什么可以改進(jìn)的,比如,是否有冗余的代碼?是否有需要增添的代碼?是否有一些結(jié)構(gòu)是我們使用類變得困難了?

7.1.1 方案A

private final Set<PersonA> knows;

在PersonA中,我們的knows是如上設(shè)計(jì)的。一開始,我們的想法是,在PersonA內(nèi)部可以訪問到他認(rèn)識(shí)的人,然后又可以從他認(rèn)識(shí)的人出發(fā),繼續(xù)向外尋找。但是在 5.2 節(jié)我們發(fā)現(xiàn),我們無法保證knows中指向的Person實(shí)例一定儲(chǔ)存了我們想要的信息。

于是,knows集合中對(duì)我們唯一有用的信息就是,認(rèn)識(shí)的其他人的名字了。
所以,是不是可以把knows改成這樣呢?

private final Set<String> knows;

同樣,addKnows和isKnows的簽名也可以改為

public void addKnows(String name);public boolean isKnows(String name);

另外,我們發(fā)現(xiàn)knowsNum()方法幾乎沒有什么用處,因?yàn)榇蠖鄶?shù)時(shí)候我們并不關(guān)心一個(gè)人認(rèn)識(shí)剁手個(gè)人。反而,我們其實(shí)需要一個(gè)getKnows方法。因?yàn)槲覀儼l(fā)現(xiàn)有些情況下,我們想要訪問一個(gè)人認(rèn)識(shí)的所有人,那么從PersonA獲得knows的一個(gè)拷貝就很有必要。

public Set<String> getKnows(){Set<String> ans = new HashSet<>();if (knows.isEmpty())return ans;ans.addAll(knows);return ans;}

7.1.2 方案B

在方案B的Graph中,我們使用了如下數(shù)據(jù)結(jié)構(gòu):

private final Set<PersonB> vertexes;private final Map<PersonB, Set<PersonB>>;

使用PersonB作為Map的鍵值的好處是,當(dāng)我們不再按照Person的name判斷兩個(gè)人是否認(rèn)識(shí)的時(shí)候,或者需要使用多種屬性判斷的時(shí)候,可以僅修改Person的equals方法,而不用對(duì)Graph做過多的修改。

如果我們要這樣做,PersonB必須是一個(gè)immutable類型。這要求PersonB內(nèi)部的值在創(chuàng)建后就不能改變。為了支持這一點(diǎn),我們沒有提供修改PersonB內(nèi)部字段的方法,而且Person字段都被private final
修飾。

7.2 通用的經(jīng)驗(yàn)

經(jīng)過這個(gè)實(shí)驗(yàn),我們學(xué)習(xí)到了許多java編程的經(jīng)驗(yàn),下面然我們來總結(jié)一下。

  • 一定要從spec(javaDoc)開始。寫spec的好處實(shí)在太多了。它記錄了方法在不同情況下的表現(xiàn),既方便他人使用,又方便自己查閱。初學(xué)者可能覺得寫不寫spec無所謂,然而當(dāng)我們要同時(shí)編寫十幾個(gè)類,類之間又有復(fù)雜的依賴關(guān)系的時(shí)候,如果我們不為每個(gè)方法寫好spec,很有可能會(huì)在編程的中途忘記某個(gè)方法的作用,或者忘記它在特殊輸入下的返回值,或者忘記某個(gè)參數(shù)的含義,或者忘記它會(huì)拋出什么異常…
  • 先寫測(cè)試代碼。如果真的要深入學(xué)習(xí)java語言,我們一定要養(yǎng)成先寫測(cè)試代碼,再完成函數(shù)的習(xí)慣。因?yàn)闇y(cè)試代碼與方法的具體實(shí)現(xiàn)是無關(guān)的,它只檢查方法的表現(xiàn)與spec所規(guī)定的是否一致。當(dāng)我們編寫好spec后,我們有很多種方法去實(shí)現(xiàn),但是只有通過測(cè)試方法,我們才能驗(yàn)證自己的實(shí)現(xiàn)是否正確。
  • 所以編程的順序是spce — 測(cè)試用例 — 實(shí)現(xiàn)。這一點(diǎn)很重要。而且每一次我們修改了自己的代碼,一定要使用測(cè)試驗(yàn)證修改的正確性。
  • 如果我們自定義了一些類,而且這些類的作用是管理數(shù)據(jù),那么,在這些類上重寫equals方法往往很有必要;而且我們可以選擇性地重寫hashCode方法。
  • 我們要避免寫出耦合度很高的程序,要盡量降低類與類之間的聯(lián)系。我們希望每個(gè)類各司其職,因?yàn)轳詈隙鹊偷拇a方便未來進(jìn)行更新修改。

7.3 后記

這篇博客基本上是跟著代碼的推進(jìn)寫下來的。

第一次使用git進(jìn)行版本管理,有的時(shí)候分支弄的比較混亂,在git.log里可以看到,有時(shí)候同一個(gè)Version我提交了很多次,就是因?yàn)閷?duì)git的使用不熟悉。如果有朋友下載了代碼,我們首先在這里說一聲抱歉。

然后其實(shí)測(cè)試代碼也有一些不是很規(guī)范的地方,但是做完之后已經(jīng)不想再去大刀闊斧地修改了。但是,基本上各種情況都覆蓋到了,如果有朋友發(fā)現(xiàn)了疏漏之處,請(qǐng)務(wù)必在評(píng)論區(qū)指出!

這篇博客是個(gè)人實(shí)驗(yàn)經(jīng)歷的分享,但是也希望能夠遇見更好的想法,歡迎大家提出自己的觀點(diǎn)。

最后,希望我走過的彎路能夠給你們帶來幫助。


參考內(nèi)容

Java中Set的contains()方法
equals()與hashCode()方法協(xié)作約定

總結(jié)

以上是生活随笔為你收集整理的【Java】一次简单实验经历——社交网络图的简化实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。