List集合物件去重及按屬性去重的8種方法-java基礎總結系列第六篇

字母哥部落格發表於2020-09-07


最近在寫一些關於java基礎的文章,但是我又不想按照教科書的方式去寫知識點的文章,因為意義不大。基礎知識太多了,如何將這些知識歸納總結,總結出優缺點或者是使用場景才是對知識的昇華。所以我更想把java相關的基礎知識進行穿針引線,進行整體上的總結。

  • 總結java中建立並寫檔案的5種方式
  • 總結java從檔案中讀取資料的6種方法
  • 總結java建立資料夾的4種方法及其優缺點
  • 總結java中刪除檔案或資料夾的7種方法
  • 總結java中檔案拷貝剪下的5種方式

比如之前我已經寫了上面的這些內容,如果對java基礎知識總結系列感興趣的同學可以關注我的部落格(文末給出我的部落格地址)

一、本文梗概

這一篇文章我想寫一下List集合元素去重的8種方法,實際上通過靈活的運用、排列組合不一定是8種,可能有18種方法。

  • 物件元素整體去重的4種方法
  • 按照物件屬性去重的4種方法

為了在下文中進行測試內容講解,我們先做一些初始化資料

public class ListRmDuplicate {
  private List<String> list;
  private List<Player> playerList;

  @BeforeEach
  public void setup() {
    list  =  new ArrayList<>();
    list.add("kobe");
    list.add("james");
    list.add("curry");
    list.add("zimug");
    list.add("zimug");

    playerList= new ArrayList<>();
    playerList.add(new Player("kobe","10000"));  //科比萬歲
    playerList.add(new Player("james","32"));
    playerList.add(new Player("curry","30"));
    playerList.add(new Player("zimug","27"));   // 注意這裡名字重複
    playerList.add(new Player("zimug","18"));   //注意這裡名字和年齡重複
    playerList.add(new Player("zimug","18")); //注意這裡名字和年齡重複

  }
}

Player物件就是一個普通的java物件,有兩個成員變數name與age,實現了帶引數建構函式、toString、equals和hashCode方法、以及GET/SET方法。

二、集合元素整體去重

下文中四種方法對List中的String型別以集合元素物件為單位整體去重。如果你的List放入的是Object物件,需要你去實現物件的equals和hashCode方法,去重的程式碼實現方法和List<String>去重是一樣的。

第一種方法

是大家最容易想到的,先把List資料放入Set,因為Set資料結構本身具有去重的功能,所以再將SET轉為List之後就是去重之後的結果。這種方法在去重之後會改變原有的List元素順序,因為HashSet本身是無序的,而TreeSet排序也不是List種元素的原有順序。

@Test
void testRemove1()  {
  /*Set<String> set = new HashSet<>(list);
  List<String> newList = new ArrayList<>(set);*/

  //去重並排序的方法(如果是字串,按字母表排序。如果是物件,按Comparable介面實現排序)
  //List<String> newList = new ArrayList<>(new TreeSet<>(list));

  //簡寫的方法
  List<String> newList = new ArrayList<>(new HashSet<>(list));

  System.out.println( "去重後的集合: " + newList);
}

控制檯列印結果如下:

去重後的集合: [kobe, james, zimug, curry]

第二種方法

使用就比較簡單,先用stream方法將集合轉換成流,然後distinct去重,最後在將Stream流collect收集為List。

@Test
void testRemove2()  {
  List<String> newList = list.stream().distinct().collect(Collectors.toList());

  System.out.println( "去重後的集合: " + newList);
}

控制檯列印結果如下:

去重後的集合: [kobe, james, curry, zimug]

第三種方法
這種方法利用了set.add(T),如果T元素已經存在集合中,就返回false。利用這個方法進行是否重複的資料判斷,如果不重複就放入一個新的newList中,這個newList就是最終的去重結果

//三個集合類list、newList、set,能夠保證順序
@Test
void testRemove3()  {

  Set<String> set = new HashSet<>();
  List<String> newList = new  ArrayList<>();
  for (String str :list) {
    if(set.add(str)){ //重複的話返回false
      newList.add(str);
    }
  }
  System.out.println( "去重後的集合: " + newList);

}

控制檯列印結果和第二種方法一致。

第四種方法
這種方法已經脫離了使用Set集合進行去重的思維,而是使用newList.contains(T)方法,在向新的List新增資料的時候判斷這個資料是否已經存在,如果存在就不新增,從而達到去重的效果。

//優化 List、newList、set,能夠保證順序
@Test
void testRemove4() {

  List<String> newList = new  ArrayList<>();
  for (String cd:list) {
    if(!newList.contains(cd)){  //主動判斷是否包含重複元素
      newList.add(cd);
    }
  }
  System.out.println( "去重後的集合: " + newList);

}

控制檯列印結果和第二種方法一致。

三、按照集合元素物件屬性去重

其實在實際的工作中,按照集合元素物件整體去重的應用的還比較少,更多的是要求我們按照元素物件的某些屬性進行去重。
看到這裡請大家回頭去看一下上文中,構造的初始化資料playerList,特別注意其中的一些重複元素,以及成員變數重複。

第一種方法
為TreeSet實現Comparator介面,如果我們希望按照Player的name屬性進行去重,就去在Comparator介面中比較name。下文中寫了兩種實現Comparator介面方法:

  • lambda表示式:(o1, o2) -> o1.getName().compareTo(o2.getName())
  • 方法引用:Comparator.comparing(Player::getName)
@Test
void testRemove5() {
  //Set<Player> playerSet = new TreeSet<>((o1, o2) -> o1.getName().compareTo(o2.getName()));
  Set<Player> playerSet = new TreeSet<>(Comparator.comparing(Player::getName));
  playerSet.addAll(playerList);

  /*new ArrayList<>(playerSet).forEach(player->{
    System.out.println(player.toString());
  });*/
  //將去重之後的結果列印出來
  new ArrayList<>(playerSet).forEach(System.out::println);
}

輸出結果如下:三個zimug因為name重複,另外兩個被去重。但是因為使用到了TreeSet,list中元素被重新排序。

Player{name='curry', age='30'}
Player{name='james', age='32'}
Player{name='kobe', age='10000'}
Player{name='zimug', age='27'}

第二種方法
這種方法是網上很多的文章中用來顯示自己很牛的方法,但是在筆者看來有點脫了褲子放屁,多此一舉。既然大家都說有這種方法,我不寫好像我不牛一樣。我為什麼說這種方法是“脫了褲子放屁”?

  • 首先用stream()把list集合轉換成流
  • 然後用collect及toCollection把流轉換成集合
  • 然後剩下的就和第一種方法一樣了

前兩步不是脫了褲子放屁麼?看看就得了,實際應用意義不大,但是如果是為了學習Stream流的使用方法,搞出這麼一個例子還是有可取之處的。

@Test
void testRemove6() {
  List<Player> newList = playerList.stream().collect(Collectors
          .collectingAndThen(
                  Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Player::getName))),
                  ArrayList::new));

  newList.forEach(System.out::println);
}

控制檯列印輸出和第一種方法一樣。

第三種方法

這種方法也是筆者建議大家使用的一種方法,咋一看好像程式碼量更大了,但實際上這種方法是應用比較簡單的方法。

Predicate(有人管這個叫斷言,從英文的角度作為名詞可以翻譯為謂詞,作為動詞可以翻譯為斷言)。謂詞就是用來修飾主語的,比如:喜歡唱歌的小鳥,喜歡唱歌就是謂詞,用來限定主語的範圍。所以我們這裡是用來filter過濾的,也是用來限制主語範圍的,所以我認為翻譯為謂詞更合適。隨便吧,看你怎麼覺得怎麼理解合理、好記,你就怎麼來。

  • 首先我們定義一個謂詞Predicate用來過濾,過濾的條件是distinctByKey。謂詞返回ture元素保留,返回false元素被過濾掉。
  • 當然我們的需求是過濾掉重複元素。我們去重邏輯是通過map的putIfAbsent實現的。putIfAbsent方法新增鍵值對,如果map集合中沒有該key對應的值,則直接新增,並返回null,如果已經存在對應的值,則依舊為原來的值。
  • 如果putIfAbsent返回null表示新增資料成功(不重複),如果putIfAbsent返回value(value==null :false),則滿足了distinctByKey謂詞的條件元素被過濾掉。

這種方法雖然看上去程式碼量增大了,但是distinctByKey謂詞方法只需要被定義一次,就可以無限複用。

@Test
void testRemove7() {
  List<Player> newList = new ArrayList<>();
  playerList.stream().filter(distinctByKey(p -> p.getName()))  //filter保留true的值
          .forEach(newList::add);

  newList.forEach(System.out::println);
}

static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
  Map<Object,Boolean> seen = new ConcurrentHashMap<>();
  //putIfAbsent方法新增鍵值對,如果map集合中沒有該key對應的值,則直接新增,並返回null,如果已經存在對應的值,則依舊為原來的值。
  //如果返回null表示新增資料成功(不重複),不重複(null==null :TRUE)
  return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}

輸出結果如下:三個zimug因為name重複,另外兩個被去重。並且沒有打亂List的原始順序

Player{name='kobe', age='10000'}
Player{name='james', age='32'}
Player{name='curry', age='30'}
Player{name='zimug', age='27'}

第四種方法
第四種方法實際上不是新方法,上面的例子都是按某一個物件屬性進行去重,如果我們想按照某幾個元素進行去重,就需要對上面的三種方法進行改造。
我只改造其中一個,另外幾個改造的原理是一樣的,就是把多個比較屬性加起來,作為一個String屬性進行比較。

@Test
void testRemove8() {
  Set<Player> playerSet = new TreeSet<>(Comparator.comparing(o -> (o.getName() + "" + o.getAge())));

  playerSet.addAll(playerList);

  new ArrayList<>(playerSet).forEach(System.out::println);
}

歡迎關注我的部落格,裡面有很多精品合集

  • 本文轉載註明出處(必須帶連線,不能只轉文字):字母哥部落格

覺得對您有幫助的話,幫我點贊、分享!您的支援是我不竭的創作動力! 。另外,筆者最近一段時間輸出瞭如下的精品內容,期待您的關注。

相關文章