zoukankan      html  css  js  c++  java
  • Java集合

    一、集合的由来

      通常,我们的程序需要根据程序运行时才知道创建多少个对象。但若非程序运行,程序开发阶段,我们根本不知道到底需要多少个数量的对象,甚至不知道它的准确类型。为了满足这些常规的编程需要,我们要求能在任何时候,任何地点创建任意数量的对象,而这些对象用什么来容纳呢?我们首先想到了数组,但是数组只能放统一类型的数据,而且其长度是固定的,那怎么办呢?集合便应运而生了!

    二、集合是什么?

      Java集合类存放于 java.util 包中,是一个用来存放对象的容器。

      注意:①、集合只能存放对象。比如你存一个 int 型数据 1放入集合中,其实它是自动转换成 Integer 类后存入的,Java中每一种基本类型都有对应的引用类型。

         ②、集合存放的是多个对象的引用,对象本身还是放在堆内存中。

         ③、集合可以存放不同类型,不限数量的数据类型

    数组和集合的比较  

      数组不是面向对象的,存在明显的缺陷,集合弥补了数组的缺点,比数组更灵活更实用,而且不同的集合框架类可适用不同场合。如下:
      1:数组能存放基本数据类型和对象,而集合类存放的都是对象的引用,而非对象本身!
      2:数组容易固定无法动态改变,集合类容量动态改变。
      3:数组无法判断其中实际存有多少元素,length只告诉了数组的容量,而集合的size()可以确切知道元素的个数
      4:集合有多种实现方式和不同适用场合,不像数组仅采用顺序表方式
      5:集合以类的形式存在,具有封装、继承、多态等类的特性,通过简单的方法和属性即可实现各种复杂操作,大大提高了软件的开发效率

    三、Java 集合框架图

    四、集合详解

    ①、Iterator:迭代器,它是Java集合的顶层接口(不包括 map 系列的集合,Map接口 是 map 系列集合的顶层接口)

      Object next():返回迭代器刚越过的元素的引用,返回值是 Object,需要强制转换成自己需要的类型
      boolean hasNext():判断容器内是否还有可供访问的元素
      void remove():删除迭代器刚越过的元素

    所以除了 map 系列的集合,我们都能通过迭代器来对集合中的元素进行遍历。
    注意:我们可以在源码中追溯到集合的顶层接口,比如 Collection 接口,可以看到它继承的是类 Iterable
    那这就得说明一下 Iterator 和 Iterable 的区别:

    Iterable :存在于 java.lang 包中。

      

    我们可以看到,里面封装 Iterator接口。所以只要实现了只要实现了Iterable接口的类,就可以使用Iterator迭代器了。

    Iterator :存在于 java.util 包中。核心的方法next(),hasnext(),remove()。
    这里我们引用一个Iterator 的实现类 ArrayList 来看一下迭代器的使用:暂时先不管 List 集合是什么,只需要看看迭代器的用法就行了

     1        //产生一个 List 集合,典型实现为 ArrayList。
     2         List list = new ArrayList();
     3         //添加三个元素
     4         list.add("Tom");
     5         list.add("Bob");
     6         list.add("Marry");
     7         //构造 List 的迭代器
     8         Iterator it = list.iterator();
     9         //通过迭代器遍历元素
    10         while(it.hasNext()){
    11             Object obj = it.next();
    12             System.out.println(obj);
    13         }

    ②、Collection:List 接口和 Set 接口的父接口

        

    看一下 Collection 集合的使用例子:

     1         //我们这里将 ArrayList集合作为 Collection 的实现类
     2         Collection collection = new ArrayList();
     3         
     4         //添加元素
     5         collection.add("Tom");
     6         collection.add("Bob");
     7         
     8         //删除指定元素
     9         collection.remove("Tom");
    10         
    11         //删除所有元素
    12         Collection c = new ArrayList();
    13         c.add("Bob");
    14         collection.removeAll(c);
    15         
    16         //检测是否存在某个元素
    17         collection.contains("Tom");
    18         
    19         //判断是否为空
    20         collection.isEmpty();
    21         
    22         //利用增强for循环遍历集合
    23         for(Object obj : collection){
    24             System.out.println(obj);
    25         }
    26         //利用迭代器 Iterator
    27         Iterator iterator = collection.iterator();
    28         while(iterator.hasNext()){
    29             Object obj = iterator.next();
    30             System.out.println(obj);
    31         }

    ③、List :有序,可以重复的集合。

      

    由于 List 接口是继承于 Collection 接口,所以基本的方法如上所示。
    1、List 接口的三个典型实现:
      ①、List list1 = new ArrayList();
        底层数据结构是数组,查询快,增删慢;线程不安全,效率高
      ②、List list2 = new Vector();
        底层数据结构是数组,查询快,增删慢;线程安全,效率低,几乎已经淘汰了这个集合
      ③、List list3 = new LinkedList();
        底层数据结构是链表,查询慢,增删快;线程不安全,效率高

    怎么记呢?我们可以想象:

      数组就像身上编了号站成一排的人,要找第10个人很容易,根据人身上的编号很快就能找到。但插入、删除慢,要望某个位置插入或删除一个人时,后面的人身上的编号都要变。当然,加入或删除的人始终末尾的也快。

      链表就像手牵着手站成一圈的人,要找第10个人不容易,必须从第一个人一个个数过去。但插入、删除快。插入时只要解开两个人的手,并重新牵上新加进来的人的手就可以。删除一样的道理。

    2、除此之外,List 接口遍历还可以使用普通 for 循环进行遍历,指定位置添加元素,替换元素等等。

     1     //产生一个 List 集合,典型实现为 ArrayList
     2         List list = new ArrayList();
     3         //添加三个元素
     4         list.add("Tom");
     5         list.add("Bob");
     6         list.add("Marry");
     7         //构造 List 的迭代器
     8         Iterator it = list.iterator();
     9         //通过迭代器遍历元素
    10         while(it.hasNext()){
    11             Object obj = it.next();
    12             //System.out.println(obj);
    13         }
    14         
    15         //在指定地方添加元素
    16         list.add(2, 0);
    17          
    18         //在指定地方替换元素
    19         list.set(2, 1);
    20          
    21         //获得指定对象的索引
    22         int i=list.indexOf(1);
    23         System.out.println("索引为:"+i);
    24         
    25         //遍历:普通for循环
    26         for(int j=0;j<list.size();j++){
    27              System.out.println(list.get(j));
    28         }    

    ④、Set:典型实现 HashSet()是一个无序,不可重复的集合

    1、Set hashSet = new HashSet();

      ①、HashSet:不能保证元素的顺序;不可重复;不是线程安全的;集合元素可以为 NULL;
      ②、其底层其实是一个数组,存在的意义是加快查询速度。我们知道在一般的数组中,元素在数组中的索引位置是随机的,元素的取值和元素的位置之间不存在确定的关系,因此,在数组中查找特定的值时,需要把查找值和一系列的元素进行比较,此时的查询效率依赖于查找过程中比较的次数。而 HashSet 集合底层数组的索引和值有一个确定的关系:index=hash(value),那么只需要调用这个公式,就能快速的找到元素或者索引。
      ③、对于 HashSet: 如果两个对象通过 equals() 方法返回 true,这两个对象的 hashCode 值也应该相同。 

    1、当向HashSet集合中存入一个元素时,HashSet会先调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据hashCode值决定该对象在HashSet中的存储位置
    1.1、如果 hashCode 值不同,直接把该元素存储到 hashCode() 指定的位置
    1.2、如果 hashCode 值相同,那么会继续判断该元素和集合对象的 equals() 作比较
    1.2.1、hashCode 相同,equals 为 true,则视为同一个对象,不保存在 hashSet()中
    1.2.2、hashCode 相同,equals 为 false,则存储在之前对象同槽位的链表上,这非常麻烦,我们应该约束这种情况,即保证:如果两个对象通过 equals() 方法返回 true,这两个对象的 hashCode 值也应该相同。

    注意:每一个存储到 哈希 表中的对象,都得提供 hashCode() 和 equals() 方法的实现,用来判断是否是同一个对象对于 HashSet 集合,我们要保证如果两个对象通过 equals() 方法返回 true,这两个对象的 hashCode 值也应该相同
    常见的 hashCode()算法:
     
     2、Set linkedHashSet = new LinkedHashSet();
      ①、不可以重复,有序
      因为底层采用 链表 和 哈希表的算法。链表保证元素的添加顺序,哈希表保证元素的唯一性
     3、Set treeSet = new TreeSet();
      TreeSet:有序;不可重复,底层使用 红黑树算法,擅长于范围查询。
    • 如果使用 TreeSet() 无参数的构造器创建一个 TreeSet 对象, 则要求放入其中的元素的类必须实现 Comparable 接口所以, 在其中不能放入 null 元素
    • 必须放入同样类的对象.(默认会进行排序) 否则可能会发生类型转换异常.我们可以使用泛型来进行限制
    1         Set treeSet = new TreeSet();
    2         treeSet.add(1);  //添加一个 Integer 类型的数据
    3         treeSet.add("a");   //添加一个 String 类型的数据
    4         System.out.println(treeSet);  //会报类型转换异常的错误

    • 自动排序:添加自定义对象的时候,必须要实现 Comparable 接口,并要覆盖 compareTo(Object obj) 方法来自定义比较规则

        如果 this > obj,返回正数 1
        如果 this < obj,返回负数 -1
        如果 this = obj,返回 0 ,则认为这两个对象相等

    • 两个对象通过 Comparable 接口 compareTo(Object obj) 方法的返回值来比较大小, 并进行升序排列
               
    • 定制排序: 创建 TreeSet 对象时, 传入 Comparator 接口的实现类. 要求: Comparator 接口的 compare 方法的返回值和 两个元素的 equals() 方法具有一致的返回值  
     1 public class TreeSetTest {
     2     public static void main(String[] args) {
     3         Person p1 = new Person(1);
     4         Person p2 = new Person(2);
     5         Person p3 = new Person(3);
     6          
     7         Set<Person> set = new TreeSet<>(new Person());
     8         set.add(p1);
     9         set.add(p2);
    10         set.add(p3);
    11         System.out.println(set);  //结果为[1, 2, 3]
    12     }
    13  
    14 }
    15  
    16 class Person implements Comparator<Person>{
    17     public int age;
    18     public Person(){}
    19     public Person(int age){
    20         this.age = age;
    21     }
    22     @Override
    23     /***
    24      * 根据年龄大小进行排序
    25      */
    26     public int compare(Person o1, Person o2) {
    27         // TODO Auto-generated method stub
    28         if(o1.age > o2.age){
    29             return 1;
    30         }else if(o1.age < o2.age){
    31             return -1;
    32         }else{
    33             return 0;
    34         }
    35     }
    36      
    37     @Override
    38     public String toString() {
    39         // TODO Auto-generated method stub
    40         return ""+this.age;
    41     }
    42 }
    •  当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保证该方法与 compareTo(Object obj) 方法有一致的结果

    以上三个 Set 接口的实现类比较:

      共同点:
        1、都不允许元素重复
        2、都不是线程安全的类,解决办法:Set set = Collections.synchronizedSet(set 对象)

      不同点:
        1、HashSet:不保证元素的添加顺序,底层采用 哈希表算法,查询效率高。判断两个元素是否相等,equals() 方法返回 true,hashCode() 值相等。即要求存入 HashSet 中的元素要覆盖 equals() 方法和 hashCode()方法
        2、LinkedHashSet:HashSet 的子类,底层采用了 哈希表算法以及 链表算法,既保证了元素的添加顺序,也保证了查询效率。但是整体性能要低于 HashSet    
        3、TreeSet:不保证元素的添加顺序,但是会对集合中的元素进行排序。底层采用 红-黑 树算法(树结构比较适合范围查询)

    ⑤、Map:key-value 的键值对,key 不允许重复,value 可以

      1、严格来说 Map 并不是一个集合,而是两个集合之间 的映射关系。
      2、这两个集合没每一条数据通过映射关系,我们可以看成是一条数据。即 Entry(key,value)。Map 可以看成是由多个 Entry 组成。
      3、因为 Map 集合即没有实现于 Collection 接口,也没有实现 Iterable 接口,所以不能对 Map 集合进行 for-each 遍历。

     1         Map<String,Object> hashMap = new HashMap<>();
     2         //添加元素到 Map 中
     3         hashMap.put("key1", "value1");
     4         hashMap.put("key2", "value2");
     5         hashMap.put("key3", "value3");
     6         hashMap.put("key4", "value4");
     7         hashMap.put("key5", "value5");
     8          
     9         //删除 Map 中的元素,通过 key 的值
    10         hashMap.remove("key1");
    11          
    12         //通过 get(key) 得到 Map 中的value
    13         Object str1 = hashMap.get("key1");
    14          
    15         //可以通过 添加 方法来修改 Map 中的元素
    16         hashMap.put("key2", "修改 key2 的 Value");
    17          
    18         //通过 map.values() 方法得到 Map 中的 value 集合
    19         Collection<Object> value = hashMap.values();
    20         for(Object obj : value){
    21             //System.out.println(obj);
    22         }
    23          
    24         //通过 map.keySet() 得到 Map 的key 的集合,然后 通过 get(key) 得到 Value
    25         Set<String> set = hashMap.keySet();
    26         for(String str : set){
    27             Object obj = hashMap.get(str);
    28             //System.out.println(str+"="+obj);
    29         }
    30          
    31         //通过 Map.entrySet() 得到 Map 的 Entry集合,然后遍历
    32         Set<Map.Entry<String, Object>> entrys = hashMap.entrySet();
    33         for(Map.Entry<String, Object> entry: entrys){
    34             String key = entry.getKey();
    35             Object value2 = entry.getValue();
    36             System.out.println(key+"="+value2);
    37         }
    38          
    39         System.out.println(hashMap);

    map常用的实现类:

      HashMap、TreeMap、LinkedHashMap、Hashtable等
      HashMap:键值对,key不能重复,但是value可以重复;key的实现就是HashSet;value对应着放;允许null的键或值;
      Hashtable:线程安全的,不允许null的键或值;
      Properties::key和value都是String类型,用来读配置文件;
      TreeMap:对key排好序的Map; key 就是TreeSet, value对应每个key; key要实现Comparable接口或TreeMap有自己的构造器;
      LinkedHashMap: 此实现与 HashMap 的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。存储的数
    据是有序的。

    ⑥、Map 和 Set 集合的关系

        1、都有几个类型的集合。HashMap 和 HashSet ,都采 哈希表算法;TreeMap 和 TreeSet 都采用 红-黑树算法;LinkedHashMap 和 LinkedHashSet 都采用 哈希表算法和红-黑树算法。
        2、分析 Set 的底层源码,我们可以看到,Set 集合 就是 由 Map 集合的 Key 组成。

     总结:

    结构图详解:

      发现一个特点,上述所有的集合类,除了 map 系列的集合,即左边集合都实现了 Iterator 接口,这是一个用于遍历集合中元素的接口,主要hashNext(),next(),remove()三种方法。它的一个子接口 ListIterator 在它的基础上又添加了三种方法,分别是 add(),previous(),hasPrevious()。也就是说如果实现 Iterator 接口,那么在遍历集合中元素的时候,只能往后遍历,被遍历后的元素不会再被遍历到,通常无序集合实现的都是这个接口,比如HashSet;而那些元素有序的集合,实现的一般都是 LinkedIterator接口,实现这个接口的集合可以双向遍历,既可以通过next()访问下一个元素,又可以通过previous()访问前一个 元素,比如ArrayList。

      还有一个特点就是抽象类的使用。如果要自己实现一个集合类,去实现那些抽象的接口会非常麻烦,工作量很大。这个时候就可以使用抽象类,这些抽象类中给我们提供了许多现成的实现,我们只需要根据自己的需求重写一些方法或者添加一些方法就可以实现自己需要的集合类,工作量大大降低。

    HashMap特点:
    Map 主要用于存储键(key)值(value)对,根据键得到值,因此键不允许重复,但允许值重复。
    HashMap 是一个最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。
    HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null;
    HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力。

    HashMap实现原理---散列
    Hash哈希算法的意义在于提供了一种快速存取数据的方法,它用一种算法建立键值与真实值之间的对应关系。散列表又称为哈希表。散列表算法的基本思想是:以结点的关键字为自变量,通过一定的函数关系(散列函数)计算出对应的函数值,以这个值作为该结点存储在散列表中地址。
    当散列表中的元素存放太满,就必须进行再散列,将产生一个新的散列表,所有元素存放到新的散列表中,原先的散列表将被删除。在Java语言中,通过负载因子(load factor)来决定何时对散列表进行再散列。例如:如果负载因子0.75,当散列表中已经有75%位置已经放满,那么将进行再散列。
    负载因子越高(越接近1.0),内存的使用效率越高,元素的寻找时间越长。负载因子越低(越接近0.0),元素的寻找时间越短,内存浪费越多。

    何时需重写equals?
    当一个类有自己特有的“逻辑相等”概念(不同于对象身份的概念);
    Object类仅仅提供了一个对引用的比较,如果两个引用不是同一个那就返回false,这是无法满足大多数对象比较的需要的,所以要覆盖;
    使用==操作符检查实参是否为指向对象的引用”
    使用instanceof操作符检查实参是否为正确的类型
    把实参转换到正确的类型;
    对于该类中每一个“关键”域,检查实参中的域与当前对象中对应的域值是否匹 配。对于既不是float也不是double类型的基本类型的域,可以使用==操作符 进行比较;对于对象引用类型的域,可以递归地调用所引用的对象的equals方法,对于float和double类型的域,先转换成int或long类型的值,然后使用==操作符比较;
    当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传 递的、一致的? 如果答案是否定的,那么请找到 这些特性未能满足的原因,再修改equals方法的代码

    equals()和hashCode()同时覆写
    尤其强调当一个对象被当作键值(或索引)来使用的时候要重写这两个方法;
    覆写equals后,两个不同实例可能在逻辑上相等,但是根据Object.hashCode方法却产生不同的散列码,违反“相等的对象必须具有相等的散列码”。
    导致,当你用其中的一个作为键保存到hashMap、hasoTable或hashSet中,再以“相等的”找另 一个作为键值去查找他们的时候,则根本找不到
    不同类型的hashCode取值
    如果该域是布尔型的,计算(f?0:1)
    如果是char,short,byte或int,计算(int)f
    如果是long类型,计算(int)(f^(f>>>32))
    如果是float类型,计算Float.floatToIntBits(f)
    如果是double类型,计算Dobule.doubleToLongBits(f)
    如果该域是一个对象引用,递归调用hashCode
    如果该域是一个数组,则把每个元素当做单独的域来处理,对每个重要的元素计算一个散列码,


    Map集合比较:
    HashMap的存入顺序和输出顺序无关。
    LinkedHashMap 则保留了键值对的存入顺序。
    TreeMap则是对Map中的元素进行排序。
    因为HashMap和LinkedHashMap 存储数据的速度比直接使用TreeMap 要快,存取效率要高。
    当完成了所有的元素的存放后,我们再对整个的Map中的元素进行排序。这样可以提高整个程序的运行的效率,缩短执行时间。
    注意:TreeMap中是根据键(Key)进行排序的。而如果我们要使用TreeMap来进行正常的排序的话,Key 中存放的对象必须实现Comparable 接口。

    Map常用方法:
    Object put(Object key,Object value):用来存放一个键-值对Map中
    Object remove(Object key):根据key(键),移除键-值对,并将值返回
    void putAll(Map mapping) :将另外一个Map中的元素存入当前的Map中
    void clear() :清空当前Map中的元素
    Object get(Object key) :根据key(键)取得对应的值
    boolean containsKey(Object key) :判断Map中是否存在某键(key)
    boolean containsValue(Object value):判断Map中是否存在某值(value)
    public Set keySet() :返回所有的键(key),并使用Set容器存放
    public Collection values() :返回所有的值(Value),并使用Collection存放
    public Set entrySet() :返回一个实现 Map.Entry 接口的元素 Set

     

     

    1.ArrayList: 元素单个,效率高,多用于查询
    2.Vector: 元素单个,线程安全,多用于查询
    3.LinkedList:元素单个,多用于插入和删除
    4.HashMap: 元素成对,元素可为空
    5.HashTable: 元素成对,线程安全,元素不可为空
    HashMap和Hashtable的区别:
    相同点:HashMap和Hashtable都是java的集合类,都可以用来存放java对象
    不同点:
    1.历史原因:Hashtable是基于陈旧的Dictionary类的,HashMap是java 1.2引进的Map接口的一个现实。
    2.同步性:Hashtable是同步的,这个类中的一些方法保证了Hashtable中的对象是线程安全的,而HashMap则是异步的,因此HashMap中的对象并不是线程安全的,因为同步的要求会影响执行的效率,所以如果你不需要线程安全的结合那么使用HashMap是一个很好的选择,这样可以避免由于同步带来的不必要的性能开销,从而提高效率,我们一般所编写的程序都是异步的,但如果是服务器端的代码除外。
    3.值:HashMap可以让你将空值作为一个表的条目的key或value,Hashtable是不能放入空值(null)的

     

    ArrayList和Vector的区别:
    相同点:ArrayList与Vector都是java的集合类,都是用来存放java对象
    不同点:
    1.同步性:Vector是同步的,这个类的一些方法保证了Vector中的对象的线程安全的,而ArrayList则是异步的,因此ArrayList中的对象并不 是线程安全的,因为同步要求会影响执行的效率,所以你不需要线程安全的集合那么使用ArrayList是一个很好的选择,这样可以避免由于同步带来的不必 要的性能开销。
    2.数据增长:从内部实现的机制来讲,ArrayList和Vector都是使用数组(Array)来控制集合中的对象,当你向两种类型中增加元素的时候,如果元素的数目超过了内部数组目前的长度他们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度,ArrayList是原来的50%,所以最后你获得的这个集合所占的空间总是比你实际需要的要大,所以如果你要在集合中保存大量的数据,那么使用Vector有一些优势,因为你可以通过设置集合的初始大小来避免不必要的资源开销。

    总结:
    1)如果要求线程安全,使用Vector,Hashtable
    2)如果不要求线程安全,使用ArrayList,LinkedList,HashMap
    3)如果要求键值对,则使用HashMap,Hashtable
    4)如果数据量很大,又要求线程安全考虑Vector

     

    arraylist和linkedlist联系与区别
    1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
    2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
    3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。 这一点要看实际情况的。若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。但若是批量随机的插入删除数据,LinkedList的速度大大优于ArrayList. 因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。

    HashMap与TreeMap联系与区别
    1、 HashMap通过hashcode对其内容进行快速查找,而TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。
    2、在Map 中插入、删除和定位元素,HashMap是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。使用HashMap要求添加的键类明确定义了hashCode()和 equals()的实现。
    两个map中的元素一样,但顺序不一样,导致hashCode()不一样。
    同样做测试:
    在HashMap中,同样的值的map,顺序不同,equals时,false;
    而在treeMap中,同样的值的map,顺序不同,equals时,true,说明,treeMap在equals()时是整理了顺序了的。

  • 相关阅读:
    nginx 报错 upstream timed out (110: Connection timed out)解决方案
    mysql 数据库缓存调优之解决The total number of locks exceeds the lock table size错误
    阿里云ECS主机内核调优
    安装Python3.6.x
    CentOS 下 LNMP 环境配置
    Walle代码发布系统
    Ansible 运维自动化 ( 配置管理工具 )
    Kafka消息的时间戳
    Linux内存分析
    H3C 查看路由器的硬件信息
  • 原文地址:https://www.cnblogs.com/BalmyLee/p/10716039.html
Copyright © 2011-2022 走看看