java-课堂笔记

集合框架使用集合做什么集合的分类具体的实现类ListArrayListLinkedListSetTreeSetMapHashMapTreeMap数组、List、Set、Map的数据转换数组转 List数组和Set从Map中得到Set和List散列表(哈希表)的基本原理散列表是如何根据Key来快速找到它所匹配的 Value 呢 ?散列表的读写操作写操作(put)读操作(get)IteratorCollections排序查找排序List 集合排序TreeMap 排序自定义类排序外部指定排序本身拥有排序

# 集合框架

程序开发并不是解决了业务的基本功能就完成了,很多时候程序运行的环境是有限制的。比如内存小,CPU频率低,或者是像手机这样的设备,能源供应有限。在这种环境下,就需要程序能够在有限的环境中提升效率。这就需要使用数据结构和算法。

但是数据结构与算法即便是学过,也未必在工作时能够用好,而且通用性、性能等等也都是问题。加上学习程序开发的受众群体越来越广,让程序员全部自己实现数据结构与算法不是一个好的主意。所以现在很多语言为了能够提升学习效率,降低学习门槛,同时也为了让程序有更好的执行效率和通用性,就自带了各种实现了数据结构与算法的API集合。在Java中,这就是我们现在要学习的「集合框架」

与现在常见到的数据结构类库一样,Java也是将集合类库的接口(interface)与实现(implementation)分离。所以我们的学习方式一般都是先搞明白接口的分类和关系,然后根据不同的接口来学习对应的实现类。

## 使用集合做什么

1.  搬运数据,集合可以存储数据,然后通过API调用很方便就可以传递大量数据

2.  数据处理,集合中可以直接对数据进行操作,比如统计、去重

3.  排序,可以将数据按照需求进行各种排序,然后再传递给调用者

## 集合的分类

Java的集合从 Collection 接口和 Map 接口入手

Map 接口和 Collection 没有交集,它有自己的方式,只要标准库后缀不是Map 结尾的,都是直接或者间接实现了Collection接口。

Collection 接口中常见的操作是数据的添加、删除

1.  add / addAll

2.  remove / removeAll / removeIf

借助 Iterator 接口,Collection 还具备了数据的循环

public interface Collection extends Iterable{ 
 //... 
 //  对数据循环
 Iterator iterator();
}
通过 Iterable 接口, 标准库中的集合都可以使用 forEach 循环。 ## 具体的实现类 | 集合类型 | 描述 | | --- | --- | | ArrayList | 一种可以动态增长和缩减的索引序列 | | LinkedList | 一种可以在任何位置进行高效地插人和删除操作的有序序列 | | ArrayDeque | 一种用循环数组实现的双端队列 | | HashSet | 一种没有重复元素的无序集合 | | TreeSet | 一种有序集 | | EnumSet | 一种包含枚举类型值的集 | | LinkedHashSet | 一种可以记住元素插人次序的集 | | PriorityQueue | 一种允许高效删除最小元素的集合 | | HashMap | 一种存储键/ 值关联的数据结构 | | TreeMap | 一种键值有序排列的映射表 | | EnumMap | 一种键值属于枚举类型的映射表 | | LinkedHashMap | 一种可以记住腱/ 值项添加次序的映射表 | | WeakHashMap | 一种其值无用武之地后可以被垃圾回收器回收的映射表 | | IdentityHashMap | 一种用 == 而不是用equals 比较键值的映射表 | **Collection** **Map** 虽然类很多 ,但是我们在教授中只需要交几个类就可以了,分别是下面: 1. ArrayList 2. LinkedList 3. HashSet 4. HashMap 5. TreeMap 然后加上工具类2个: 1. Collections 2. Arrays ## List 有序集合,可以精确控制列表中每个元素的插入位置。通过整数索引获取列表中的元素。List允许出现重复的值 , 并可以精确控制列表中每个元素的插入位置,通过整数索引获取列表中的元素。 | 方法名 | 说明 | | --- | --- | | add(E e) | 增加单个数据 | | addAll(Collection c) | 将一个 Collection 集合的数据添加到现在的集合中 | | remove(Object o) | 删除指定的元素 | | contains(Object o) | 判断集合是否包含指定的元素 | | size() | 得到集合的数据总数 | | isEmpty() | 判断集合是否有数据 | | get(int index) | 通过索引获取对应的数据元素 | | set(int index, E element) | 通过索引和新的元素替换原有内容 | | clear() | 清空数据 | | toArray() | 将List转为对象数组 | ### ArrayList ArrayList 是List 接口的大小可变数组的实现。实现了所有可选列表操作, 并允许包括null 在内的所有元素。除了实现List 接口外, 此类还提供一些方法来操作内部用来存储列表的数组的大小( 此类大致上等同于 vector 类, 但 vector 是同步的) 。 ArrayList 的底层是使用数组实现的,看下面的图 可以看到,数组中的每一个元素,都存储在内存单元中,并且元素之间紧密排列,既不能打乱元素的存储顺序,也不能跳过某个存储单元进行存储。 ArrayList 底层既然是使用数组实现,那么特点就和数组一致:**查询速度快,增删速度慢**。 每个ArrayList 实例都有一个容量。该容量是指用来存储列表元素的数组的大小, 它总是至少等于列表的大小。随着向Array L ist 中小断添加元素, 其容量也自动增长。并未指定增长策略的细节,因为这不只是添加元素会带来分摊固定时间开销那样简单。我们可以使用默认构造函数创建容量为 10 的列表, 也可以初始化指定容量大小。 **ArrayList 指定初始容量大小的构造器方法**
public ArrayList(int initialCapacity) {
 if (initialCapacity > 0) {
 this.elementData = new Object[initialCapacity];
 } else if (initialCapacity == 0) {
 this.elementData = EMPTY_ELEMENTDATA;
 } else {
 throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
 }
}
**常用的操作**
public static void main(String[] args) {

 ArrayList list = new ArrayList<>();

 //  添加数据
 list.add(123);
 list.add(346);

 //  替换数据
 list.set(1, 777);

 //  将list中的所有数据放到 list2 中
 List list2 = new ArrayList<>();
 list2.addAll( list );

 //  循环list2中所有的数据
 for (Integer integer : list2) {

 System.out.println( integer );

 //  删除循环出的对象
 list2.remove(integer);
 }

 //  list 集合是否有数据
 if( !list.isEmpty() ) {

 System.out.println("list.size = "+ list.size());

 //  清空 list
 list.clear();
 }

 //  在清空list后,再看list现在有多少数据
 System.out.println("list.size = "+ list.size());
}
### LinkedList LinkedList 是一个链表结构,可当作堆栈、队列、双端队列 。链表是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成。 **单链表** 单向链表的每一个节点包含两部分,一部分是存放数据的变量data,另一部分是指向下一个节点的指针next。正如地下党的联络方式,一级一级,单线传递:
private static class Node {
 int data;
 Node next;
}
**双链表** 双向链表比单向链表稍微复杂一些,它的每一个节点除了拥有data和next指针,还拥有指向前置节点的prev指针 和数组不同,链表存储数据的时候,则采用了见缝插针的方式,链表的每一个节点分布在内存的不同位置,依靠next指针关联起来。这样可以灵活有效地利用零散的碎片空间。 LinkedList 做**数据的添加和删除操作时速度很快,但是做查询的时候速度就很慢**,这和 ArrayList 刚好相反。 适用 LinkedList 独有的方法,**在集合前后做数据插入**
public static void main(String[] args) {

 LinkedList list = new LinkedList<>();

 list.add(123);
 //  添加相同的数据
 list.add(123);
 list.add(456);
 list.add(789);
 list.add(101);

 //  将数据 111 添加到 list 的末尾
 list.addLast(111);
 //  将数据 999 添加到 list的最前面
 list.addFirst(999);

 for (int i = 0; i < list.size(); i++) {
 System.out.println( list.get(i) );
 }

}
> 注意代码中声明和实现都是 LinkedList , 如果声明的是 List 接口 , addFirst 和 addLast 方法是不可见的 ## Set Set 的特点是去重,如果相同的数据只会保留一个。集合内的数据是一个无序的状态,。当然也有例外,TreeSet就是有序的。 | 方法名 | 说明 | | --- | --- | | add(E e) | 增加单个数据 | | addAll(Collection c) | 将一个 Collection 集合的数据添加到现在的集合中 | | remove(Object o) | 删除指定的元素 | | contains(Object o) | 判断集合是否包含指定的元素 | | size() | 得到集合的数据总数 | | isEmpty() | 判断集合是否有数据 | | clear() | 清空数据 | | toArray() | 将List转为对象数组 | 可以看到,Set 和 List 中的方法都是相同的,作用也一致。但是 Set 是没有直接获取数据的方法。我们更多的时候使用的是 List 和 Map。 Has hSet 由哈希表( 实际上是一个HashM ap 实例) 支持, 为基木操作提供了稳定性能, 这些基本操作包括add() 、remove() 、contains() 和size() , 假定哈希函数将这些元素正确地分布在桶中。对此 set() 进行迭代所需的时间与HashSet 实例的大小( 元素的数量) 和底层HashMap 实例( 桶的数量)的“ 容量” 的和成比例。因此, 如果迭代性能很重要, 则不要将初始容量没置得太高( 或将加载因了设置得太低) 。
public static void main(String[] args) {

 HashSet set = new HashSet<>();

 set.add( new Object() );
 set.add( new Object() );
 set.add( "hello" );
 set.add( "hello" );
 set.add( "你好" );
 set.add( 123 );
 set.add( 123 );
 set.add( 234 );
 set.remove("你好");
 set.remove( new Object() );
 for (Object object : set) {
 System.out.println( object );
 }
}
以上代码经输出可以看到:输出的内容没有按照输入时的顺序进行输出。
李四
8890
张三
azxcv
ABC
### TreeSet Set 集合中的另类,存储的数据时有序的
public static void main(String[] args) {

 TreeSet set = new TreeSet<>();
 set.add("8890");
 set.add("123");
 set.add("张三");
 set.add("李四");
 set.add("ABC");
 set.add("azxcv");

 for (String string : set) {
 System.out.println( string );
 }
}
输出的结果:
123
8890
ABC
azxcv
张三
李四
可以发现数据是按照数字、大写字母、小写字母、汉字进行排序的,但是汉字相关的数据本身没有排序,可以尝试将李四放在张三的数据上面看输出的结果。 ## Map Map 是一种把键对象和值对象进行关联的容器, 而一个值对象又可以是一个Map, 依次类推,这样就可形成一个多级映射。 > 想想学习英语使用的词典软件,输入英文(key)后,软件会显示出对应的中文(value)。Map就是在内存中的这种结构。 **Key(键):** 1. 和 set— 样,键对象不允许重复,这是为了保持查找结果的一致性。 如果有两个键对象一样, 那你想得到那个键对象所对应的值对象时就有问题了。 2. 在使用过程中某个键所对应的值对象可能会发生变化, 这时会按照最后一次修改的值对象与键对应(就是key同一个key有多次值绑定,最后一个就会覆盖之前的) 3. 可以使用 null 作为 Key **Value(值):** 1. 值对象没有唯一性的要求, 你可以将任意多个键都映射到一个值对象上, 这不会发生任何问题( 不过对使用却可能会造成不便, 你不知道你得到的到底是那一个键所对应的值对象,所以请不要这样做) 2. 可以使用 null 作为 Value > Map 有两种比较常用的实现: HashMap 和 TreeMap **常用的方法** | 方法名 | 说明 | | --- | --- | | put(key , value) | 储存数据 | | get(key) | 通过key得到值 | | remove(key) | 通过key删除对应的值(key当然也会删除) | | entrySet() | 获取Map所有的Key,返回一个Set集合 | | values() | 获取Map所有的value,返回一个List 集合 | | containsKey(key) | 判断Map中是否有输入的参数:key | | containsValue(value) | 判断Map中是否有输入的参数:value | | size() | 判断Map中数据的总数 | | clear() | 清空Map中所有的数据 | | isEmpty() | 判断Map中是否有数据 | ### HashMap HashMap 用到了哈希码的算法, 以便快速查找一个键。
public static void main(String[] args) {

 HashMap zsInfo = new HashMap<>();

 zsInfo.put("name", "张三");
 zsInfo.put("height", "173CM");
 zsInfo.put("sex", "男性");

 for (Map.Entry info : zsInfo.entrySet()) {
 System.out.println( info );
 }
}
### TreeMap TreeMap 是对键按序存放, 因此它便有一些扩展的方法, 比如 firstKey() 、lastKey() 等, 可以从TreeMap 中指定一个范围以取得其子Map
public static void main(String[] args) {

 TreeMap tree = new TreeMap<>();

 tree.put("name", "Jack");
 tree.put("age", "22");
 tree.put("身高", "173");
 tree.put("sex", "man");
 tree.put("体重", "70KG");

 System.out.println("-------------------");
 for (Map.Entry entry : tree.entrySet()) {
 System.out.println( entry );
 }
 System.out.println("-------------------");

 System.out.println("firstKey = "+ tree.firstKey());
 System.out.println("firstEntry = "+ tree.firstEntry());
 System.out.println("lastKey = "+ tree.lastKey());
 System.out.println("lastEntry = "+ tree.lastEntry());

}
## 数组、List、Set、Map的数据转换 不同类型的集合相互转换在程序开发中都是非常常见的 API 操作,这些基础操作一定要熟悉。 ### 数组转 List
/*
 * 数组转List,但是这里要注意,Arrays.asList方法转出来的虽然也叫ArrayList,
 * 但不是java.util.ArrayList,而是Arrays中的一个内部类ArrayList。两者之间并不相等。
 * 内部类ArrayList没有实现remove、add等方法,使用的话会报错。
 */
System.out.println("使用Arrays.asList将数组转换为List--------------------------------");
String str[] = { "中文", "计算机", "ABC", "123", "[email protected]" };

List strList = Arrays.asList(str);

for (String string : strList) {
 System.out.println(string);
}

System.out.println("使用ArrayList构造器方式再由Arrays.asList将数组转换为List----------");
// 使用下面的代码的写法就不会有问题了。
ArrayList stringsList = new ArrayList(Arrays.asList(str));
// 使用remove方法
stringsList.remove(3);

for (String string : stringsList) {
 System.out.println(string);
}

System.out.println("使用List.toArray方法将一个List转换为数组(默认为Object数组)----------");
Object[] strArr = stringsList.toArray();
for (Object object : strArr) {
 System.out.println(object.toString());
}
### 数组和Set
System.out.println("使用Arrays.asList将数组转换为List--------------------------------");
String str[] = { "中文", "计算机", "ABC", "123", "[email protected]" };

System.out.println("下面是数组转Set ----------------------------------");

Set strSet = new HashSet(Arrays.asList(str));
for (String string : strSet) {
 System.out.println(string);
}
System.out.println("下面是将Set集合转换为数组-------------");
Object[] objArr = strSet.toArray();
for (Object string : objArr) {
 System.out.println(string);
}
### 从Map中得到Set和List
Map map = new HashMap();
map.put("1", "A");
map.put("2", "B");
map.put("3", "C");

// 输出所有的key , keySet()方法其实返回的就是一个Set集合。
System.out.println("通过keySet输出所有的key:" + map.keySet());

// 把map的值转为一个List
List stringList = new LinkedList(map.values());
for (String string : stringList) {
 System.out.print(string + " ");
}
## 散列表(哈希表)的基本原理 上面 HashSet 和 HashMap 都使用了 散列表来做数据的检索,那么什么是散列表 ,这个又有什么优点呢 ? 散列表也叫作哈希表(hash table),这种数据结构提供了键(Key)和值(Value)的映射关系。只要给出一个Key,就可以高效查找到它所匹配的Value,时间复杂度接近于O(1)。 > 是不是想到了 Map ? ### 散列表是如何根据Key来快速找到它所匹配的 Value 呢 ? 在目前所学的数据结构中,数组的查询效率是最高的。散列表本质就是一个数组。不同的是**数组的下标是数字**,但是**散列表的下标是字符串**。这就需要设计一个中转站,通过某种方式,**把Key和数组下标进行转换。这个中转站就叫作哈希函数**。 在不同的语言中,哈希函数的实现是不一样的。我们以Java的HashMap为例,来看看哈希函数的实现。 在Java及大多数面向对象的语言中,每一个对象都有属于自己的hashcode,这个hashcode是区分不同对象的重要标识。无论对象自身的类型是什么,它们的hashcode都是一个整型变量。 既然都是整型变量,想要转化成数组的下标也就不难实现了。最简单的转化方式是什么呢?是按照数组长度进行取模运算
index = HashCode (Key) % Array.length
> 其实JDK中的哈希函数为了提升效率,使用的是位运算。我们就假设使用的是模运算 通过哈希函数,我们可以把字符串或其他类型的Key,转化成数组的下标index。如给出一个长度为8的数组,则当 key=001121时
index = HashCode ("001121") % Array.length = 1420036703 % 8 = 7
而当key=this时,
index = HashCode ("this") % Array.length = 3559070 % 8 = 6
### 散列表的读写操作 有了哈希函数,就可以在散列表中进行读写操作了。 #### 写操作(put) 写操作就是在散列表中插入新的键值对(在JDK中叫作Entry)。如调用hashMap.put("002931", "王五"),意思是插入一组Key为002931、Value为王五的键值对。具体该怎么做呢? 1. 通过哈希函数,把Key转化成数组下标5。 2. 如果数组下标5对应的位置没有元素,就把这个Entry填充到数组下标5的位置 由于数组的长度是有限的,当插入的Entry越来越多时,不同的Key通过哈希函数获得的下标有可能是相同的。例如002936这个Key对应的数组下标是2;002947这个Key对应的数组下标也是2。 **这种情况,就叫作哈希冲突** 解决哈希冲突的方法主要有两种,一种是开放寻址法,一种是链表法 1. 开放寻址法的原理很简单,当一个Key通过哈希函数获得对应的数组下标已被占用时,我们可以“另谋高就”,寻找下一个空档位置 2. HashMap数组的每一个元素不仅是一个Entry对象,还是一个链表的头节点。每一个Entry对象通过next指针指向它的下一个Entry节点。当新来的Entry映射到与之冲突的数组位置时,只需要插入到对应的链表中即可 #### 读操作(get) 读操作就是通过给定的Key,在散列表中查找对应的Value。例如调用 hashMap.get("002936"),意思是查找Key为002936的Entry在散列表中所对应的值 1. 通过哈希函数,把Key转化成数组下标2。 2. 找到数组下标2所对应的元素,如果这个元素的Key是002936,那么就找到了;如果这个Key不是002936也没关系,由于数组的每个元素都与一个链表对应,我们可以顺着链表慢慢往下找,看看能否找到与Key相匹配的节点。 ## Iterator iterator是为了实现对Java容器(collection)进行遍历功能的一个接口。 在iterator实现了Iterator接口后,相当于把一个Collection容器的所有对象,做成一个线性表(List),而iterator本身是一个指针,开始时位于第一个元素之前。 | 方法 | 说明 | | --- | --- | | hasNext() | 判断 iterator 内是否存在下1个元素,如果存在,返回true,否则返回false。(注意,这时上面的那个指针位置不变) | | next() | 返回 iterator 内下1个元素,同时上面的指针向后移动一位。如果不断地循环执行next()方法,就可以遍历容器内所有的元素了 | | remove() | 删除 iterator 内指针的前1个元素,前提是至少执行过1次next() | 遍历1个ArrayList 和Linklist是十分容易的,遍历1个Tree容器也不难,但是实现机制是完全不同,而遍历1个Set容器就无从下手了。 Iterator 这个接口让各种容器自己去重写里面的hasNext()和next()方法。 不用关心各种容器的遍历机制,只要使用Iterator,会让人觉得各种容器的遍历方法都是一样的,这就是Java接口的重要意义。
HashMap zsInfo = new HashMap<>();

zsInfo.put("name", "张三");
zsInfo.put("height", "173CM");
zsInfo.put("sex", "男性");
zsInfo.put("skin-color", "白色");

zsInfo.put("height", "183CM");
zsInfo.remove("skin-color");

System.out.println("Iterator 循环------------------");
Iterator iterator = zsInfo.entrySet().iterator();

while( iterator.hasNext() ) {
 Map.Entry next = (Entry) iterator.next();
 System.out.println( next );
}
## Collections Collections 是 Java 提供对Set 、List 和Map 等集合操作的工具类。 该工具类里提供了大量方法,除了对集合元素进行排序、查询和修改等操作;还提供了将集合对象设置为不可变、对集合对象实现同步控制等方法。 ### 排序
ArrayList nums = new ArrayList<>();
nums.add(2);
nums.add(0);
nums.add(-5);
nums.add(0);
nums.add(3);
System.out.println( nums );
Collections.reverse( nums );
System.out.println( nums);

Collections.sort( nums );
System.out.println( nums );

Collections.shuffle( nums );
System.out.println( nums );
### 查找
ArrayList nums = new ArrayList<>();
nums.add(2);
nums.add(0);
nums.add(-5);
nums.add(0);
nums.add(3);
System.out.println( nums );

System.out.println( Collections.max( nums ));
System.out.println( Collections.min( nums ));
Collections.replaceAll(nums, 0 , 1);
System.out.println( nums );

System.out.println( Collections.frequency(nums, 1));

Collections.sort(nums);
System.out.println( nums );

System.out.println( Collections.binarySearch(nums, 3 ));
## 排序 集合的排序有2种方式: 1. 集合内部就拥有排序的能力。 需要排序的能力的类实现 Comparable 接口的方法 2. 集合本身没有排序的能力,可以通过外部指定排序的方式。 在排序方法中,指定一个实现了 Comparator 接口的类的对象 Comparable是需要比较的对象来实现接口。这样对象调用实现的方法来比较。对对象的耦合度高(需要改变对象的内部结构,破坏性大)。 Comparator相当于一通用的比较工具类接口。需要定制一个比较类去实现它,重写里面的compare方法,方法的参数即是需要比较的对象。对象不用做任何改变,解耦。 ### List 集合排序
List list = new ArrayList();

list.add(123);
list.add(999);
list.add(101);
list.add(33);
list.add(76);

Collections.sort(list);

for (Integer integer : list) {
 System.out.println( integer );
}
直接就可以得到自然升序,我们在上面的代码中没有看到任何接口,原因是List集合中的 Integer 类本身就实现了 Comparable 接口
public final class Integer extends Number implements Comparable {
 //...
}
### TreeMap 排序 treeMap 的示例中,我们使用了外部指定排序方式:Comparator
TreeMap tree = new TreeMap<>(new Comparator() {
 @Override
 public int compare(String o1, String o2) {
 return o2.compareTo(o1) ;  // 
 }
});

tree.put("name", "Jack");
tree.put("age", "22");
tree.put("身高", "173");
tree.put("sex", "man");
tree.put("体重", "70KG");

System.out.println("-------------------");
for (Map.Entry entry : tree.entrySet()) {
 System.out.println( entry );
}
### 自定义类排序 当我们自定义了一个类,也希望能够进行排序的话,就需要实现 Comparator接口或者是 Comparable 接口了。
public class User {
 private Integer uid;
 private String uname;
 //....
}
#### 外部指定排序 User 类没有实现 Comparable 接口,通过 Collections 工具类的 sort 方法进行排序,在 sort 方法的第二个参数上指定排序的规则
List list = new ArrayList();

list.add( new User(110 ,"Mark") );
list.add( new User(101 ,"李四") );
list.add( new User(100 ,"张三") );
list.add( new User(111 ,"Jack") );

//  Comparator 接口因为是外部排序,所以需要知道对比的2个对象
Collections.sort(list , new Comparator() {
 @Override
 public int compare(User o1, User o2) {
 return o2.getUid() - o1.getUid() ;
 }
});

for (User user : list) {
 System.out.println( user );
}
#### 本身拥有排序 因为是内部排序,所以只需要传入被排序的对象即可,另一个排序对象当然是对象本身,使用 this 指向即可。
public class User implements Comparable {

    private Integer uid;
    private String uname;

    public User(int i, String string) {
        this.uid = i;
        this.uname = string;
    }

    @Override
    public int compareTo(User o) {
        return this.getUid() - o.getUid() ;
    }

    public Integer getUid() {
        return uid;
    }

    public void setUid(Integer uid) {
        this.uid = uid;
    }

    public String getUname() {
        return uname;
    }

    public void setUname(String uname) {
        this.uname = uname;
    }

    @Override
    public String toString() {
        return "User [uid=" + uid + ", uname=" + uname + "]";
    }
}
排序
List list = new ArrayList();

list.add( new User(110 ,"Mark") );
list.add( new User(101 ,"李四") );
list.add( new User(100 ,"张三") );
list.add( new User(111 ,"Jack") );

Collections.sort(list);

for (User user : list) {
    System.out.println( user );
}
输出结果
User [uid=100, uname=张三]
User [uid=101, uname=李四]
User [uid=110, uname=Mark]
User [uid=111, uname=Jack]

你可能感兴趣的:(java-课堂笔记)