Collection 集合 集合概述 在 Java 基础(中篇) 已经学习过并使用过集合 ArrayList<E>
,那么集合到底是什么呢?集合是 java 中提供的一种容器,可以用来存储多个数据。那么集合和数组既然都是容器,它们之间有什么区别呢?
数组和集合的区别:
1、数组的长度是固定的。集合的长度是可变的。
2、数组中存储的是同一类型的元素,可以存储基本数据类型值,也可以存储对象。集合存储的都是对象,而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。
来看下图,了解集合:
集合框架 JavaSE 提供了满足各种需求的 API,在使用这些 API 前,先了解其继承与接口操作架构,才能了解何时采用哪个类,以及类之间如何彼此合作,从而达到灵活应用。
集合按照其存储结构可以分为两大类,分别是单列集合 java.util.Collection
和双列集合 java.util.Map
。
Collection 集合:
1、Collection 集合:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是 List 和 Set 。
2、List 集合:List 的特点是 元素有序、元素可重复 。List 的主要实现类有 ArrayList 和 LinkedList。
3、Set 集合:Set 的特点是 元素无序,而且不可重复 。Set 的主要实现类有 HashSet 和 TreeSet。
从上面的描述可以看出 JDK 中提供了丰富的集合类库,为了便于进行系统地学习,接下来通过一张图来描述整个集合类的继承体系。
其中,橙色框代表接口类型,而蓝色框代表具体的实现类。集合本身是一个工具,它存放在 java.util 包中。在 Collection 接口中定义着单列集合框架中最最共性的内容。
Collection 常用功能 Collection 是所有单列集合的父接口,因此在 Collection 中定义了单列集合( List 和 Set ) 通用 的一些方法,这些方法可用于操作所有的单列集合。这些通用方法如下:
public boolean add(E e)
: 把给定的对象添加到当前集合中 。
public void clear()
:清空集合中所有的元素。
public boolean remove(E e)
: 把给定的对象在当前集合中删除。
public boolean contains(E e)
: 判断当前集合中是否包含给定的对象。
public boolean isEmpty()
: 判断当前集合是否为空。
public int size()
: 返回集合中元素的个数。
public Object[] toArray()
: 把集合中的元素,存储到数组中。
代码实例演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public static void main (String[] args) { Collection<String> collection = new ArrayList<>(); System.out.println(collection); boolean bool01 = collection.add("张三" ); System.out.println(bool01); System.out.println(collection); collection.add("李四" ); collection.add("李四" ); collection.add("王五" ); collection.add("赵六" ); System.out.println(collection); boolean bool02 = collection.remove("Jack" ); System.out.println(bool02); boolean bool03 = collection.remove("李四" ); System.out.println(bool03); System.out.println(collection); boolean bool04 = collection.contains("Jack" ); System.out.println(bool04); boolean bool05 = collection.contains("张三" ); System.out.println(bool05); boolean isEmpty = collection.isEmpty(); System.out.println(isEmpty); int size = collection.size(); System.out.println(size); System.out.println(collection); Object[] objects = collection.toArray(); for (int i = 0 ; i < objects.length; i++) { System.out.println(objects[i]); } collection.clear(); System.out.println(collection); System.out.println(collection.isEmpty()); }
迭代器知识 迭代器介绍 在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK 专门提供了一个接口 java.util.Iterator
。Iterator 接口也是 Java 集合中的一员,但它与 Collection、Map 接口有所不同,Collection 接口与 Map 接口主要用于存储元素,而 Iterator 主要用于迭代访问(即遍历)Collection 中的元素,因此 Iterator 对象也被称为迭代器。想要遍历 Collection 集合,那么就要获取该集合迭代器完成迭代操作。
迭代的概念:
迭代是集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。
Iterator 接口的常用方法如下:
public Iterator iterator()
: 获取集合对应的迭代器,用来遍历集合中的元素的。
public E next()
:返回迭代的下一个元素。
public boolean hasNext()
:如果仍有元素可以迭代,则返回 true。
迭代器使用 接下来我们通过代码学习如何使用 Iterator 迭代集合中元素,使用代码把上述介绍的方法过一遍:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public static void main (String[] args) { Collection<String> collection = new ArrayList<>(); collection.add("Lisa" ); collection.add("Jack" ); collection.add("Tom" ); collection.add("Jerry" ); Iterator<String> iterator = collection.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } System.out.println("===========" ); for (Iterator<String> it = collection.iterator(); it.hasNext();){ System.out.println(it.next()); } Collection<String> coll = new ArrayList<>(); Iterator<String> iterator1 = coll.iterator(); System.out.println(iterator1.next()); }
迭代器原理 我们在上述案例已经完成了 Iterator 遍历集合的整个过程。当遍历集合时,首先通过调用集合的 iterator()
方法获得迭代器对象,然后使用 hashNext()
方法判断集合中是否存在下一个元素,如果存在,则调用 next()
方法将元素取出,否则说明已到达了集合末尾,停止遍历元素。Iterator 迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素,为了能更好地理解迭代器的工作原理,接下来通过一个图例来演示 Iterator 对象迭代元素的过程:
在调用 Iterator 的 next 方法之前,迭代器的索引位于第一个元素之前,即上图的 -1 位置,不指向任何元素,当第一次调用迭代器的 next 方法后,迭代器的索引会向后移动一位,指向第一个元素并将该元素返回,当再次调用 next 方法时,迭代器的索引会指向第二个元素并将该元素返回,依此类推,直到 hasNext 方法返回 false ,表示到达了集合的末尾,终止对元素的遍历。
增强 for 循环 所有的解释都在注释里面了,请看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public static void main (String[] args) { ArrayList<String> list = new ArrayList<>(); list.add("abc" ); list.add("def" ); list.add("ghi" ); list.add("jkl" ); for (String str : list) { System.out.println(str); } System.out.println("=====" ); int [] arr = {1 , 2 , 3 , 4 , 5 }; for (int i : arr){ System.out.println(i); } }
泛型 泛型的概述 Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?答案是可以使用 Java 泛型。使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。
看一张图,了解泛型:
泛型的好处
泛型的好处:
1、将运行时期的 ClassCastException,转移到了编译时期变成了编译失败。
2、避免了类型强转的麻烦。
通过如下代码体验一下泛型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public static void main (String[] args) { listDemoSecond(); } private static void listDemoSecond () { ArrayList<String> arrayList = new ArrayList<>(); arrayList.add("Tom" ); arrayList.add("Lisa" ); arrayList.add("Jack" ); Iterator<String> it = arrayList.iterator(); while (it.hasNext()){ String string = it.next(); System.out.println(string + " --> " + string.length()); } } private static void listDemoFirst () { ArrayList list = new ArrayList(); list.add("Jack" ); list.add(37 ); Iterator iterator = list.iterator(); while (iterator.hasNext()){ Object object = iterator.next(); System.out.println(object); String str = (String)object; System.out.println(str.length()); } }
tips:泛型是数据类型的一部分,我们将类名与泛型合并一起看做数据类型。
泛型的定义和使用 我们在集合中会大量使用到泛型,这里来完整地学习泛型知识。泛型,用来灵活地将数据类型应用到不同的类、方法、接口当中。将数据类型作为参数进行传递。
有泛型的类 例如,API 中的 ArrayList 集合:
1 2 3 4 5 6 7 class ArrayList <E > { public boolean add (E e) { } public E get (int index) { } .... }
实际代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class MyArrayList <E > { private E name; public E getName () { return name; } public void setName (E name) { this .name = name; } }
使用自定义的泛型类: 即什么时候确定泛型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static void main (String[] args) { MyArrayList myArrayList = new MyArrayList(); myArrayList.setName("Jack" ); Object object = myArrayList.getName(); System.out.println(object); MyArrayList<Integer> integerMyArrayList = new MyArrayList<>(); integerMyArrayList.setName(520 ); System.out.println(integerMyArrayList.getName()); MyArrayList<String> stringMyArrayList = new MyArrayList<>(); stringMyArrayList.setName("Tom" ); System.out.println(stringMyArrayList.getName()); }
有泛型的方法 例如:
1 2 3 4 5 6 7 8 9 10 public class MyGenericMethod { public <MVP> void show (MVP mvp) { System.out.println(mvp.getClass()); } public <MVP> MVP show2 (MVP mvp) { return mvp; } }
具体定义代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public <M> void method01 (M m) { System.out.println(m); } public static <S> void method02 (S s) { System.out.println(s); }
使用格式:调用方法时,确定泛型的类型 ,相关代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class GenericMethod { public static void main (String[] args) { GenericMethod genericMethod = new GenericMethod(); genericMethod.method01(10 ); genericMethod.method01("abc" ); genericMethod.method01(3.14 ); genericMethod.method01(true ); GenericMethod.method02("静态方法" ); GenericMethod.method02(1.732 ); } public <M> void method01 (M m) { System.out.println(m); } public static <S> void method02 (S s) { System.out.println(s); } }
有泛型的接口 定义格式及示例如下:
1 2 3 4 5 6 public interface MyGenericInterface <E > { public abstract void add (E e) ; public abstract E getE () ; }
含有泛型的接口的具体代码如下:
1 2 3 4 public interface GenericInterface <E > { public abstract void method (E e) ; }
泛型接口的第一种实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class GenericInterfaceImpl1 implements GenericInterface <String > { @Override public void method (String str) { System.out.println(str); } }
泛型接口的第二种实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class GenericInterfaceImpl2 <E > implements GenericInterface <E > { @Override public void method (E e) { System.out.println(e); } }
测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Demo04GenericInterface { public static void main (String[] args) { GenericInterfaceImpl1 gi1 = new GenericInterfaceImpl1(); gi1.method("字符串" ); GenericInterfaceImpl2<Integer> gi2 = new GenericInterfaceImpl2<>(); gi2.method(10 ); GenericInterfaceImpl2<Double> gi3 = new GenericInterfaceImpl2<>(); gi3.method(8.8 ); } }
泛型通配符 当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符 <?> 表示。但是一旦使用泛型的通配符后,只能使用 Object 类中的共性方法,集合中元素自身方法无法使用。
通配符基本使用 泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用 ?, ? 表示未知通配符。 此时只能接受数据,不能往该集合中存储数据。举个例子大家理解使用即可:
1 2 3 4 5 6 7 8 9 public static void main (String[] args) { Collection<Integer> list1 = new ArrayList<Integer>(); getElement(list1); Collection<String> list2 = new ArrayList<String>(); getElement(list2); } public static void getElement (Collection<?> coll) {}
具体实际代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public static void main (String[] args) { ArrayList<Integer> list01 = new ArrayList<>(); list01.add(1 ); list01.add(2 ); ArrayList<String> list02 = new ArrayList<>(); list02.add("a" ); list02.add("b" ); printArray(list01); printArray(list02); } private static void printArray (ArrayList<?> list) { Iterator<?> iterator = list.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } }
tips:泛型不存在继承关系 Collection<Object> list = new ArrayList<String>(); 这种是错误的。
通配符高级之受限泛型 之前设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在 Java 的泛型中可以指定一个泛型的 上限 和 下限 。
泛型的上限:
1、格式: 类型名称 < ? extends xxx类 > 对象名称
2、意义: 只能接收 xxx类型 及其子类
泛型的下限:
1、格式: 类型名称 < ? super xxx类 > 对象名称
2、意义: 只能接收 xxx类型 及其父类型
比如:现已知 Object 类,String 类,Number 类,Integer 类,其中 Number 是 Integer 的父类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static void main (String[] args) { Collection<Integer> list1 = new ArrayList<Integer>(); Collection<String> list2 = new ArrayList<String>(); Collection<Number> list3 = new ArrayList<Number>(); Collection<Object> list4 = new ArrayList<Object>(); getElement1(list1); getElement1(list2); getElement1(list3); getElement1(list4); getElement2(list1); getElement2(list2); getElement2(list3); getElement2(list4); } public static void getElement1 (Collection<? extends Number> coll) {}public static void getElement2 (Collection<? super Number> coll) {}
斗地主案例(单列) 案例介绍 按照斗地主的规则,完成洗牌发牌的动作。具体规则: 使用 54 张牌打乱顺序,三个玩家参与游戏,三人交替摸牌,每人 17 张牌,最后三张留作底牌。
案例分析
准备牌:
牌可以设计为一个 ArrayList ,每个字符串为一张牌。
每张牌由花色数字两部分组成,我们可以使用花色集合与数字集合嵌套迭代完成每张牌的组装。
牌由 Collections 类的 shuffle 方法进行随机排序。
发牌:
将每个人以及底牌设计为 ArrayList , 将最后 3 张牌直接存放于底牌,剩余牌通过对 3 取模依次发牌。
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public class DouDiZhu { public static void main (String[] args) { ArrayList<String> poker = new ArrayList<>(); String[] colors = {"♠" ,"♥" ,"♣" ,"♦" }; String[] numbers = {"2" ,"A" ,"K" ,"Q" ,"J" ,"10" ,"9" ,"8" ,"7" ,"6" ,"5" ,"4" ,"3" }; poker.add("大王" ); poker.add("小王" ); for (String number : numbers){ for (String color : colors) { poker.add(color + number); } } Collections.shuffle(poker); ArrayList<String> player01 = new ArrayList<>(); ArrayList<String> player02 = new ArrayList<>(); ArrayList<String> player03 = new ArrayList<>(); ArrayList<String> diPai = new ArrayList<>(); for (int i = 0 ; i < poker.size() ; i++) { String p = poker.get(i); if (i >= 51 ){ diPai.add(p); }else if (i % 3 == 0 ){ player01.add(p); }else if (i % 3 == 1 ){ player02.add(p); }else if (i % 3 == 2 ){ player03.add(p); } } System.out.println("刘德华:" + player01); System.out.println("周润发:" + player02); System.out.println("周星驰:" + player03); System.out.println("底牌:" + diPai); } }
Java 数据结构 栈
栈(stack):
1、概念:栈又称堆栈,它是运算受限的线性表,仅允许在一端进行插入和删除操作,不允许在其他任何位置进行添加、删除等操作。
2、特点:先进后出(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素)。
3、举例:子弹压进弹夹,先压进去的子弹在下面,后压进去的子弹在上面,当开枪时,先打出上面的子弹,然后才能打出下面的子弹。
4、出入口:栈的入口、出口的都是栈的顶端位置。
5、压栈:就是存元素。即,把元素存储到栈的顶端位置,栈中已有元素依次向栈底方向移动一个位置。
6、弹栈:就是取元素。即,把栈的顶端位置元素取出,栈中已有元素依次向栈顶方向移动一个位置。
队列
队列(queue):
1、概念:队列简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。
2、特点:先进先出(即,先存进去的元素先取出来,后存进去的元素后取出)
3、举例:例如,小火车过山洞,车头先进去,车尾后进去;车头先出来,车尾后出来。
4、出入口:队列的入口、出口各占一侧。就像管道一样,一边进一边出。
数组
数组(Array):
1、概念:是有序的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素。
2、特点:查找元素快(通过索引,可以快速访问指定位置的元素),增删元素慢。
3、举例:就像是一排出租屋,有100个房间,从001到100每个房间都有固定编号,通过编号就可以快速找到租房子的人。
链表
链表(linked list):
1、概念:链表由一系列结点 node(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
2、特点:查找元素慢(想查找某个元素,需要通过连接的节点,依次向后查找指定元素)。增删元素快,增加元素:只需要修改连接下个元素的地址即可。删除元素:只需要修改连接下个元素的地址即可。
3、举例:多个结点之间,通过地址进行连接。例如,多个人手拉手,每个人使用自己的右手拉住下个人的左手,依次类推,这样多个人就连在一起了。
红黑树
二叉树(binary tree):
1、二叉树概念:二叉树是每个节点最多有两个子树的树结构。顶上的叫根结点,两边被称作“左子树”和“右子树”。简单的理解,就是一种类似于我们生活中树的结构,只不过每个结点上都最多只能有两个子结点。
2、红黑树概念:红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。
2、红黑树特点:速度特别快,趋近平衡树,查找叶子元素最少和最多次数不多于二倍。
红黑树约束:
1、节点可以是红色的或者黑色的
2、根节点是黑色的
3、叶子节点(特指空节点)是黑色的
4、每个红色节点的子节点都是黑色的
5、任何一个节点到其每一个叶子节点的所有路径上黑色节点数相同
这里写的数据结构只作为了解和参考,并没有什么实际用途。此处也没有写非常详细的东西,所以不要太在意错误或者其他无法理解的地方。如果要看数据结构,很详细的数据结构,请移步: 数据结构入门
List 集合 我们掌握了 Collection 接口的使用后,再来看看 Collection 接口中的子类,他们都具备那些特性呢?接下来,我们一起学习 Collection 中的常用几个子类(java.util.List
集合、java.util.Set
集合)。
List接口介绍 java.util.List
接口继承自Collection
接口,是单列集合的一个重要分支,习惯性地会将实现了List
接口的对象称为 List 集合。在 List 集合中允许出现重复的元素,所有的元素是以一种线性方式进行存储的,在程序中可以通过索引来访问集合中的指定元素。另外,List 集合还有一个特点就是元素有序,即元素的存入顺序和取出顺序一致。
看完API,我们总结一下 List 接口特点:
它是一个元素存取有序的集合。例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)。
它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素。
tips:我们已经学习过 List 接口的子类 java.util.ArrayList 类,该类中的方法都是来自 List 中定义。
List 中常用方法 List 作为 Collection 集合的子接口,不但继承了 Collection 接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法,如下:
public void add(int index, E element)
: 将指定的元素,添加到该集合中的指定位置上。
public E get(int index)
:返回集合中指定位置的元素。
public E remove(int index)
: 移除列表中指定位置的元素, 返回的是被移除的元素。
public E set(int index, E element)
:用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
List集合特有的方法都是跟索引相关,我们已经都学习过,那么我们再来复习一遍吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 public class Demo01List { public static void main (String[] args) { List<String> list = new ArrayList<>(); list.add("a" ); list.add("b" ); list.add("c" ); list.add("d" ); list.add("a" ); System.out.println(list); list.add(3 ,"itheima" ); System.out.println(list); String removeE = list.remove(2 ); System.out.println("被移除的元素:" +removeE); System.out.println(list); String setE = list.set(4 , "A" ); System.out.println("被替换的元素:" +setE); System.out.println(list); for (int i=0 ; i<list.size(); i++){ String s = list.get(i); System.out.println(s); } System.out.println("-----------------" ); Iterator<String> it = list.iterator(); while (it.hasNext()){ String s = it.next(); System.out.println(s); } System.out.println("-----------------" ); for (String s : list) { System.out.println(s); } String r = list.get(5 ); System.out.println(r); } }
ArrayList 集合 java.util.ArrayList
集合数据存储的结构是数组结构。元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以ArrayList
是最常用的集合。许多程序员开发时非常随意地使用 ArrayList 完成任何需求,并不严谨,这种用法是不提倡的。
想要了解更多的信息,请参阅 API 或者查看他人博客。也可看先前文章:https://guoshizhan.club/JavaSE-02.html
LinkedList 集合 java.util.LinkedList
集合数据存储的结构是链表结构。方便元素添加、删除的集合。
LinkedList是一个双向链表,那么双向链表是什么样子的呢,我们用个图了解下
实际开发中对一个集合元素的添加与删除经常涉及到首尾操作,而LinkedList提供了大量首尾操作的方法。这些方法我们作为了解即可:
public void addFirst(E e)
:将指定元素插入此列表的开头。
public void addLast(E e)
:将指定元素添加到此列表的结尾。
public E getFirst()
:返回此列表的第一个元素。
public E getLast()
:返回此列表的最后一个元素。
public E removeFirst()
:移除并返回此列表的第一个元素。
public E removeLast()
:移除并返回此列表的最后一个元素。
public E pop()
:从此列表所表示的堆栈处弹出一个元素。
public void push(E e)
:将元素推入此列表所表示的堆栈。
public boolean isEmpty()
:如果列表不包含元素,则返回true。
LinkedList是List的子类,List中的方法LinkedList都是可以使用,这里就不做详细介绍,我们只需要了解LinkedList的特有方法即可。在开发时,LinkedList集合也可以作为堆栈,队列的结构使用。(了解即可)
方法演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 public class Demo02LinkedList { public static void main (String[] args) { show03(); } private static void show03 () { LinkedList<String> linked = new LinkedList<>(); linked.add("a" ); linked.add("b" ); linked.add("c" ); System.out.println(linked); String first = linked.pop(); System.out.println("被移除的第一个元素:" +first); String last = linked.removeLast(); System.out.println("被移除的最后一个元素:" +last); System.out.println(linked); } private static void show02 () { LinkedList<String> linked = new LinkedList<>(); linked.add("a" ); linked.add("b" ); linked.add("c" ); if (!linked.isEmpty()){ String first = linked.getFirst(); System.out.println(first); String last = linked.getLast(); System.out.println(last); } } private static void show01 () { LinkedList<String> linked = new LinkedList<>(); linked.add("a" ); linked.add("b" ); linked.add("c" ); System.out.println(linked); linked.push("www" ); System.out.println(linked); linked.addLast("com" ); System.out.println(linked); } }
Vector 集合 这个集合比较老了,是从 JDK1.0 版本就有的。在此只是去知道它的存在,具体用法差不多,不懂就查 Java 编程手册。
Map 集合 Map 概述 现实生活中,我们常会看到这样的一种集合:IP 地址与主机名,身份证号与个人,系统用户名与系统用户对象等,这种一一对应的关系,就叫做映射。Java 提供了专门的集合类用来存放这种对象关系的对象,即 java.util.Map 接口。
Map 常用子类 通过查看 Map 接口描述,看到 Map 有多个子类,这里我们主要讲解常用的 HashMap 集合、LinkedHashMap 集合。
HashMap:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
LinkedHashMap:HashMap下有个子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。通过链表结构可以保证元素的存取顺序一致;通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
tips:Map 接口中的集合都有两个泛型变量,在使用时,要为两个泛型变量赋予数据类型。两个泛型变量的数据类型可以相同,也可以不同。
Map 的常用方法 Map 的代码演示如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 public class Demo01Map { public static void main (String[] args) { show04(); } private static void show04 () { Map<String,Integer> map = new HashMap<>(); map.put("赵丽颖" ,168 ); map.put("杨颖" ,165 ); map.put("林志玲" ,178 ); boolean b1 = map.containsKey("赵丽颖" ); System.out.println("b1:" +b1); boolean b2 = map.containsKey("赵颖" ); System.out.println("b2:" +b2); } private static void show03 () { Map<String,Integer> map = new HashMap<>(); map.put("赵丽颖" ,168 ); map.put("杨颖" ,165 ); map.put("林志玲" ,178 ); Integer v1 = map.get("杨颖" ); System.out.println("v1:" +v1); Integer v2 = map.get("迪丽热巴" ); System.out.println("v2:" +v2); } private static void show02 () { Map<String,Integer> map = new HashMap<>(); map.put("赵丽颖" ,168 ); map.put("杨颖" ,165 ); map.put("林志玲" ,178 ); System.out.println(map); Integer v1 = map.remove("林志玲" ); System.out.println("v1:" +v1); System.out.println(map); Integer v2 = map.remove("林志颖" ); System.out.println("v2:" +v2); System.out.println(map); } private static void show01 () { Map<String,String> map = new HashMap<>(); String v1 = map.put("李晨" , "范冰冰1" ); System.out.println("v1:" +v1); String v2 = map.put("李晨" , "范冰冰2" ); System.out.println("v2:" +v2); System.out.println(map); map.put("冷锋" ,"龙小云" ); map.put("杨过" ,"小龙女" ); map.put("尹志平" ,"小龙女" ); System.out.println(map); } }
tips:使用put方法时,若指定的键(key)在集合中没有,则没有这个键对应的值,返回null,并把指定的键值添加到集合中;若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值),并把指定键所对应的值,替换成指定的新值。
Map 遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public class Demo02KeySet { public static void main (String[] args) { Map<String,Integer> map = new HashMap<>(); map.put("赵丽颖" ,168 ); map.put("杨颖" ,165 ); map.put("林志玲" ,178 ); Set<String> set = map.keySet(); Iterator<String> it = set.iterator(); while (it.hasNext()){ String key = it.next(); Integer value = map.get(key); System.out.println(key+"=" +value); } System.out.println("-------------------" ); for (String key : set){ Integer value = map.get(key); System.out.println(key+"=" +value); } System.out.println("-------------------" ); for (String key : map.keySet()){ Integer value = map.get(key); System.out.println(key+"=" +value); } } }
Entry键值对对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class Demo03EntrySet { public static void main (String[] args) { Map<String,Integer> map = new HashMap<>(); map.put("赵丽颖" ,168 ); map.put("杨颖" ,165 ); map.put("林志玲" ,178 ); Set<Map.Entry<String, Integer>> set = map.entrySet(); Iterator<Map.Entry<String, Integer>> it = set.iterator(); while (it.hasNext()){ Map.Entry<String, Integer> entry = it.next(); String key = entry.getKey(); Integer value = entry.getValue(); System.out.println(key+"=" +value); } System.out.println("-----------------------" ); for (Map.Entry<String,Integer> entry:set){ String key = entry.getKey(); Integer value = entry.getValue(); System.out.println(key+"=" +value); } } }
HashMap 存储自定义类型键值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public class Person { private String name; private int age; public Person () { } public Person (String name, int age) { this .name = name; this .age = age; } @Override public String toString () { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}' ; } @Override public boolean equals (Object o) { if (this == o) return true ; if (o == null || getClass() != o.getClass()) return false ; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } @Override public int hashCode () { return Objects.hash(name, age); } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public class Demo01HashMapSavePerson { public static void main (String[] args) { show02(); } private static void show02 () { HashMap<Person,String> map = new HashMap<>(); map.put(new Person("女王" ,18 ),"英国" ); map.put(new Person("秦始皇" ,18 ),"秦国" ); map.put(new Person("普京" ,30 ),"俄罗斯" ); map.put(new Person("女王" ,18 ),"毛里求斯" ); Set<Map.Entry<Person, String>> set = map.entrySet(); for (Map.Entry<Person, String> entry : set) { Person key = entry.getKey(); String value = entry.getValue(); System.out.println(key+"-->" +value); } } private static void show01 () { HashMap<String,Person> map = new HashMap<>(); map.put("北京" ,new Person("张三" ,18 )); map.put("上海" ,new Person("李四" ,19 )); map.put("广州" ,new Person("王五" ,20 )); map.put("北京" ,new Person("赵六" ,18 )); Set<String> set = map.keySet(); for (String key : set) { Person value = map.get(key); System.out.println(key+"-->" +value); } } }
LinkedHashMap 我们知道HashMap保证成对元素唯一,并且查询速度很快,可是成对元素存放进去是没有顺序的,那么我们要保证有序,还要速度快怎么办呢?在HashMap下面有一个子类LinkedHashMap,它是链表和哈希表组合的一个数据存储结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Demo01LinkedHashMap { public static void main (String[] args) { HashMap<String,String> map = new HashMap<>(); map.put("a" ,"a" ); map.put("c" ,"c" ); map.put("b" ,"b" ); map.put("a" ,"d" ); System.out.println(map); LinkedHashMap<String,String> linked = new LinkedHashMap<>(); linked.put("a" ,"a" ); linked.put("c" ,"c" ); linked.put("b" ,"b" ); linked.put("a" ,"d" ); System.out.println(linked); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class Demo02Hashtable { public static void main (String[] args) { HashMap<String,String> map = new HashMap<>(); map.put(null ,"a" ); map.put("b" ,null ); map.put(null ,null ); System.out.println(map); Hashtable<String,String> table = new Hashtable<>(); table.put(null ,null ); } }
Map集合练习 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class Demo03MapTest { public static void main (String[] args) { Scanner sc = new Scanner(System.in); System.out.println("请输入一个字符串:" ); String str = sc.next(); HashMap<Character,Integer> map = new HashMap<>(); for (char c :str.toCharArray()){ if (map.containsKey(c)){ Integer value = map.get(c); value++; map.put(c,value); }else { map.put(c,1 ); } } for (Character key :map.keySet()){ Integer value = map.get(key); System.out.println(key+"=" +value); } } }
JDK9 新特性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class Demo01JDK9 { public static void main (String[] args) { List<String> list = List.of("a" , "b" , "a" , "c" , "d" ); System.out.println(list); Set<String> set = Set.of("a" , "b" , "c" , "d" ); System.out.println(set); Map<String, Integer> map = Map.of("张三" , 18 , "李四" , 19 , "王五" , 20 ); System.out.println(map); } }
Debug 调试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class Demo01Debug { public static void main (String[] args) { print(); } private static void print () { System.out.println("HelloWorld" ); System.out.println("HelloWorld" ); System.out.println("HelloWorld" ); System.out.println("HelloWorld" ); System.out.println("HelloWorld" ); } }
斗地主案例(双列) 案例介绍 按照斗地主的规则,完成洗牌发牌的动作。具体规则如下:
组装 54 张扑克牌将
54 张牌顺序打乱
三个玩家参与游戏,三人交替摸牌,每人 17 张牌,最后三张留作底牌。
查看三人各自手中的牌(按照牌的大小排序)、底牌
规则:手中扑克牌从大到小的摆放顺序:大王,小王,2,A,K,Q,J,10,9,8,7,6,5,4,3
案例分析
准备牌: 完成数字与纸牌的映射关系:使用双列Map(HashMap)集合,完成一个数字与字符串纸牌的对应关系(相当于一个字典)。
洗牌: 通过数字完成洗牌发牌
发牌: 将每个人以及底牌设计为ArrayList,将最后3张牌直接存放于底牌,剩余牌通过对3取模依次发牌。存放的过程中要求数字大小与斗地主规则的大小对应。将代表不同纸牌的数字分配给不同的玩家与底牌。
看牌: 通过Map集合找到对应字符展示。通过查询纸牌与数字的对应关系,由数字转成纸牌字符串再进行展示。
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 public class DouDiZhu { public static void main (String[] args) { HashMap<Integer,String> poker = new HashMap<>(); ArrayList<Integer> pokerIndex = new ArrayList<>(); List<String> colors = List.of("♠" , "♥" , "♣" , "♦" ); List<String> numbers = List.of("2" , "A" , "K" , "Q" , "J" , "10" , "9" , "8" , "7" , "6" , "5" , "4" , "3" ); int index = 0 ; poker.put(index,"大王" ); pokerIndex.add(index); index++; poker.put(index,"小王" ); pokerIndex.add(index); index++; for (String number : numbers) { for (String color : colors) { poker.put(index,color+number); pokerIndex.add(index); index++; } } Collections.shuffle(pokerIndex); ArrayList<Integer> player01 = new ArrayList<>(); ArrayList<Integer> player02 = new ArrayList<>(); ArrayList<Integer> player03 = new ArrayList<>(); ArrayList<Integer> diPai = new ArrayList<>(); for (int i = 0 ; i <pokerIndex.size() ; i++) { Integer in = pokerIndex.get(i); if (i>=51 ){ diPai.add(in); }else if (i%3 ==0 ){ player01.add(in); }else if (i%3 ==1 ){ player02.add(in); }else if (i%3 ==2 ){ player03.add(in); } } Collections.sort(player01); Collections.sort(player02); Collections.sort(player03); Collections.sort(diPai); lookPoker("刘德华" ,poker,player01); lookPoker("周润发" ,poker,player02); lookPoker("周星驰" ,poker,player03); lookPoker("底牌" ,poker,diPai); } public static void lookPoker (String name,HashMap<Integer,String> poker,ArrayList<Integer> list) { System.out.print(name+":" ); for (Integer key : list) { String value = poker.get(key); System.out.print(value+" " ); } System.out.println(); } }
异常处理 异常概念 异常 :就是不正常的意思,指的是程序在执行过程中,出现的非正常的情况,最终会导致 JVM 的非正常停止。在 Java 等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java 处理异常的方式是中断处理。
异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行。
异常体系 异常机制其实是帮助我们找到 程序中的问题,异常的根类是 java.lang.Throwable
,其下有两个子类:java.lang.Error
与 java.lang.Exception
,平常所说的异常指 java.lang.Exception
。如下图:
Throwable 常用方法 以下三个方法都是为了获取异常信息:
public void printStackTrace()
: 打印异常的详细信息。包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用 printStackTrace。public String getMessage()
:获取发生异常的原因。提示给用户的时候,就提示错误原因。public String toString()
:获取异常的类型和异常描述信息(不用)。
异常分类
编译时期异常 : checked 异常。在编译时期,就会检查,如果没有处理异常,则编译失败。(如日期格式化异常)运行时期异常 : runtime 异常。在运行时期,检查异常.在编译时期,运行异常不会编译器检测(不报错)。(如数学异常)
异常处理 Java异常处理的五个关键字:try、catch、finally、throw、throws
抛出异常 throw 在编写程序时,我们必须要考虑程序出现问题的情况。比如,在定义方法时,方法需要接受参数。那么,当调用方法使用接受到的参数时,首先需要先对参数数据进行合法的判断,数据若不合法,就应该告诉调用者,传递合法的数据进来。这时需要使用抛出异常的方式来告诉调用者。在 java 中,提供了一个 throw 关键字,它用来抛出一个指定的异常对象。那么,抛出一个异常具体如何操作呢?
使用格式:
例如:
1 2 3 throw new NullPointerException("要访问的arr数组不存在" );throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围" );
学习完抛出异常的格式后,我们通过下面程序演示下 throw 的使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class ThrowDemo { public static void main (String[] args) { int [] arr = {2 ,4 ,52 ,2 }; int index = 4 ; int element = getElement(arr, index); System.out.println(element); System.out.println("over" ); } public static int getElement (int [] arr,int index) { if (index<0 || index>arr.length-1 ){ throw new ArrayIndexOutOfBoundsException("哥们,角标越界了~~~" ); } int element = arr[index]; return element; } }
注意:如果产生了问题,我们就会 throw 将问题描述类即异常进行抛出,也就是将问题返回给该方法的调用者。
那么对于调用者来说,该怎么处理呢?一种是进行捕获处理,另一种就是继续讲问题声明出去,使用 throws 声明处理。
声明异常 throws 声明异常 :将问题标识出来,报告给调用者。如果方法内通过 throw 抛出了编译时异常,而没有捕获处理(下面讲解该方式),那么必须通过 throws 进行声明,让调用者去处理。关键字 throws 运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常).
声明异常格式:
1 修饰符 返回值类型 方法名(参数) throws 异常类名1 ,异常类名2 …{ }
声明异常的代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class ThrowsDemo { public static void main (String[] args) throws FileNotFoundException { read("a.txt" ); } public static void read (String path) throws FileNotFoundException { if (!path.equals("a.txt" )) { throw new FileNotFoundException("文件不存在" ); } } }
throws用于进行异常类的声明,若该方法可能有多种异常情况产生,那么在throws后面可以写多个异常类,用逗号隔开。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class ThrowsDemo2 { public static void main (String[] args) throws IOException { read("a.txt" ); } public static void read (String path) throws FileNotFoundException, IOException { if (!path.equals("a.txt" )) { throw new FileNotFoundException("文件不存在" ); } if (!path.equals("b.txt" )) { throw new IOException(); } } }
捕获异常 try_catch 如果异常出现的话,会立刻终止程序,所以我们得处理异常:
1、该方法不处理,而是声明抛出,由该方法的调用者来处理(throws)。
2、如果要处理,那就在方法中使用 try-catch 的语句块来捕获异常。
try-catch 的方式就是捕获异常【Java 中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理】,语法如下:
1 2 3 4 5 6 try { 编写可能会出现异常的代码 }catch (异常类型 e){ 处理异常的代码 }
try: 在该代码块中,编写可能产生异常的代码。catch: 用来进行某种异常的捕获,实现对捕获到的异常进行处理。注意 : try 和 catch 都不能单独使用,必须连用。
演示如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class TryCatchDemo { public static void main (String[] args) { try { read("b.txt" ); } catch (FileNotFoundException e) { System.out.println(e); } System.out.println("over" ); } public static void read (String path) throws FileNotFoundException { if (!path.equals("a.txt" )) { throw new FileNotFoundException("文件不存在" ); } } }
finally 代码块 finally :有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而 finally 就是解决这个问题的,在 finally 代码块中存放的代码都是一定会被执行的。
什么时候的代码必须最终执行?当我们在 try 语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都得在使用完之后,最终关闭打开的资源。
finally 的语法 : try…catch….finally:自身需要处理异常,最终还得关闭资源。
finally 代码参考如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class TryCatchDemo4 { public static void main (String[] args) { try { read("a.txt" ); } catch (FileNotFoundException e) { throw new RuntimeException(e); } finally { System.out.println("不管程序怎样,这里都将会被执行。" ); } System.out.println("over" ); } public static void read (String path) throws FileNotFoundException { if (!path.equals("a.txt" )) { throw new FileNotFoundException("文件不存在" ); } } }
注意事项:
1、 finally 不能单独使用。 2、 当只有在 try 或者 catch 中调用退出 JVM 的相关方法,此时 finally 才不会执行,否则 finally 永远会执行。
异常注意事项
多个异常使用捕获又该如何处理呢?
多个异常分别处理。
多个异常一次捕获,多次处理。
多个异常一次捕获一次处理。
一般我们是使用一次捕获多次处理方式,格式如下:
1 2 3 4 5 6 7 8 9 try { }catch (异常类型A e){ }catch (异常类型B e){ 处理异常的代码 }
注意事项:
1、这种异常处理方式,要求多个 catch 中的异常不能相同,并且若 catch 中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的 catch 处理,父类异常在下面的 catch 处理。 2、运行时异常被抛出可以不处理。即不捕获也不声明抛出。 3、如果 finally 有 return 语句,永远返回 finally 中的结果,避免该情况。 4、如果父类抛出了多个异常,子类重写父类方法时,抛出和父类相同的异常或者是父类异常的子类或者不抛出异常。 5、父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出。
自定义异常 自定义异常概述 为什么需要自定义异常类:
我们说了Java中不同的异常类,分别表示着某一种具体的异常情况,那么在开发中总是有些异常情况是SUN没有定义好的,此时我们根据自己业务的异常情况来定义异常类。例如年龄负数问题,考试成绩负数问题等等。
在上述代码中,发现这些异常都是JDK内部定义好的,但是实际开发中也会出现很多异常,这些异常很可能在JDK中没有定义过,例如年龄负数问题,考试成绩负数问题.那么能不能自己定义异常呢?
什么是自定义异常类:
在开发中根据自己业务的异常情况来定义异常类.
自定义一个业务逻辑异常: RegisterException 。一个注册异常类。
异常类如何定义:
自定义一个编译期异常: 自定义类并继承于 java.lang.Exception
。
自定义一个运行时期的异常类:自定义类 并继承于 java.lang.RuntimeException
。
自定义异常的练习 要求:我们模拟注册操作,如果用户名已存在,则抛出异常并提示:亲,该用户名已经被注册。
首先定义一个登陆异常类 RegisterException:
1 2 3 4 5 6 7 8 9 10 11 public class RegisterException extends Exception { public RegisterException () { } public RegisterException (String message) { super (message); } }
模拟登陆操作,使用数组模拟数据库中存储的数据,并提供当前注册账号是否存在方法用于判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class Demo { private static String[] names = {"bill" ,"hill" ,"jill" }; public static void main (String[] args) { try { checkUsername("nill" ); System.out.println("注册成功" ); }catch (RegisterException e){ e.printStackTrace(); } } public static boolean checkUsername (String uname) throws LoginException { for (String name : names) { if (name.equals(uname)){ throw new RegisterException("亲" + name + "已经被注册了!" ); } } return true ; } }
多线程 了解概念 并发和并行
并发 :指两个或多个事件在同一个时间段内 发生。例如:一个手机同时玩王者和吃鸡,只能交替进行。并行 :指两个或多个事件在同一时刻 发生(同时发生)。例如:边打游戏边听歌,同时进行。
在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。
而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。
注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
线程与进程
进程 :是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程 :线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
线程调度:
多线程原理
常用方法
设置线程的名称
多线程休眠方法有点意思,每隔一秒执行,打印一分钟
Runnable接口创建线程对象
Runnable接口创建线程对象和thread创建对象的区别
线程安全
买票重复,出现-1
解决线程安全的三种方法
线程池 线程池介绍 涉及到一个概念,面向接口编程,就是返回对象是接口类型,不用管接口的实现类
concurrent executors
Lambda 表达式 函数式编程思想概述
在数学中,函数 就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做 。
面向对象的思想: 做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情.
函数式编程思想: 只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程
冗余的 Runnable 代码 传统写法 当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable
接口来定义任务内容,并使用java.lang.Thread
类来启动该线程。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 public class Demo01Runnable { public static void main (String[] args) { Runnable task = new Runnable() { @Override public void run () { System.out.println("多线程任务执行!" ); } }; new Thread(task).start(); } }
本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable
接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。
代码分析 对于Runnable
的匿名内部类用法,可以分析出几点内容:
Thread
类需要Runnable
接口作为参数,其中的抽象run
方法是用来指定线程任务内容的核心;
为了指定run
的方法体,不得不 需要Runnable
接口的实现类;
为了省去定义一个RunnableImpl
实现类的麻烦,不得不 使用匿名内部类;
必须覆盖重写抽象run
方法,所以方法名称、方法参数、方法返回值不得不 再写一遍,且不能写错;
而实际上,似乎只有方法体才是关键所在 。
编程思想转换 做什么,而不是怎么做 我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而不得不 创建一个对象。我们真正希望做的事情是:将run
方法体内的代码传递给Thread
类知晓。
传递一段代码 ——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。
生活举例 当我们需要从北京到上海时,可以选择高铁、汽车、骑行或是徒步。我们的真正目的是到达上海,而如何才能到达上海的形式并不重要,所以我们一直在探索有没有比高铁更好的方式——搭乘飞机。
而现在这种飞机(甚至是飞船)已经诞生:2014年3月Oracle所发布的Java 8(JDK 1.8)中,加入了Lambda表达式 的重量级新特性,为我们打开了新世界的大门。
体验Lambda的更优写法 借助Java 8的全新语法,上述Runnable
接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效:
1 2 3 4 5 public class Demo02LambdaRunnable { public static void main (String[] args) { new Thread(() -> System.out.println("多线程任务执行!" )).start(); } }
这段代码和刚才的执行效果是完全一样的,可以在1.8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担,就是这么简单!
回顾匿名内部类 Lambda是怎样击败面向对象的?在上例中,核心代码其实只是如下所示的内容:
1 () -> System.out.println("多线程任务执行!" )
为了理解Lambda的语义,我们需要从传统的代码起步。
使用实现类 要启动一个线程,需要创建一个Thread
类的对象并调用start
方法。而为了指定线程执行的内容,需要调用Thread
类的构造方法:
public Thread(Runnable target)
为了获取Runnable
接口的实现对象,可以为该接口定义一个实现类RunnableImpl
:
1 2 3 4 5 6 public class RunnableImpl implements Runnable { @Override public void run () { System.out.println("多线程任务执行!" ); } }
然后创建该实现类的对象作为Thread
类的构造参数:
1 2 3 4 5 6 public class Demo03ThreadInitParam { public static void main (String[] args) { Runnable task = new RunnableImpl(); new Thread(task).start(); } }
使用匿名内部类 这个RunnableImpl
类只是为了实现Runnable
接口而存在的,而且仅被使用了唯一一次,所以使用匿名内部类的语法即可省去该类的单独定义,即匿名内部类:
1 2 3 4 5 6 7 8 9 10 public class Demo04ThreadNameless { public static void main (String[] args) { new Thread(new Runnable() { @Override public void run () { System.out.println("多线程任务执行!" ); } }).start(); } }
匿名内部类的好处与弊端 一方面,匿名内部类可以帮我们省去实现类的定义 ;另一方面,匿名内部类的语法——确实太复杂了!
语义分析 仔细分析该代码中的语义,Runnable
接口只有一个run
方法的定义:
public abstract void run();
即制定了一种做事情的方案(其实就是一个函数):
无参数 :不需要任何条件即可执行该方案。
无返回值 :该方案不产生任何结果。
代码块 (方法体):该方案的具体执行步骤。
同样的语义体现在Lambda
语法中,要更加简单:
1 () -> System.out.println("多线程任务执行!" )
前面的一对小括号即run
方法的参数(无),代表不需要任何条件;
中间的一个箭头代表将前面的参数传递给后面的代码;
后面的输出语句即业务逻辑代码。
Lambda标准格式 Lambda省去面向对象的条条框框,格式由3个部分 组成:
Lambda表达式的标准格式 为:
格式说明:
小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
->
是新引入的语法格式,代表指向动作。
大括号内的语法与传统方法体要求基本一致。
练习:使用Lambda标准格式(无参无返回) 题目 给定一个厨子Cook
接口,内含唯一的抽象方法makeFood
,且无参数、无返回值。如下:
1 2 3 public interface Cook { void makeFood () ; }
在下面的代码中,请使用Lambda的标准格式 调用invokeCook
方法,打印输出“吃饭啦!”字样:
1 2 3 4 5 6 7 8 9 public class Demo05InvokeCook { public static void main (String[] args) { } private static void invokeCook (Cook cook) { cook.makeFood(); } }
解答 1 2 3 4 5 public static void main (String[] args) { invokeCook(() -> { System.out.println("吃饭啦!" ); }); }
备注:小括号代表Cook
接口makeFood
抽象方法的参数为空,大括号代表makeFood
的方法体。
Lambda的参数和返回值 1 2 3 需求: 使用数组存储多个Person对象 对数组中的Person对象使用Arrays的sort方法通过年龄进行升序排序
下面举例演示java.util.Comparator<T>
接口的使用场景代码,其中的抽象方法定义为:
public abstract int compare(T o1, T o2);
当需要对一个对象数组进行排序时,Arrays.sort
方法需要一个Comparator
接口实例来指定排序的规则。假设有一个Person
类,含有String name
和int age
两个成员变量:
1 2 3 4 5 6 public class Person { private String name; private int age; }
传统写法 如果使用传统的代码对Person[]
数组进行排序,写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import java.util.Arrays;import java.util.Comparator;public class Demo06Comparator { public static void main (String[] args) { Person[] array = { new Person("古力娜扎" , 19 ), new Person("迪丽热巴" , 18 ), new Person("马尔扎哈" , 20 ) }; Comparator<Person> comp = new Comparator<Person>() { @Override public int compare (Person o1, Person o2) { return o1.getAge() - o2.getAge(); } }; Arrays.sort(array, comp); for (Person person : array) { System.out.println(person); } } }
这种做法在面向对象的思想中,似乎也是“理所当然”的。其中Comparator
接口的实例(使用了匿名内部类)代表了“按照年龄从小到大”的排序规则。
代码分析 下面我们来搞清楚上述代码真正要做什么事情。
为了排序,Arrays.sort
方法需要排序规则,即Comparator
接口的实例,抽象方法compare
是关键;
为了指定compare
的方法体,不得不 需要Comparator
接口的实现类;
为了省去定义一个ComparatorImpl
实现类的麻烦,不得不 使用匿名内部类;
必须覆盖重写抽象compare
方法,所以方法名称、方法参数、方法返回值不得不 再写一遍,且不能写错;
实际上,只有参数和方法体才是关键 。
Lambda写法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.util.Arrays;public class Demo07ComparatorLambda { public static void main (String[] args) { Person[] array = { new Person("古力娜扎" , 19 ), new Person("迪丽热巴" , 18 ), new Person("马尔扎哈" , 20 ) }; Arrays.sort(array, (Person a, Person b) -> { return a.getAge() - b.getAge(); }); for (Person person : array) { System.out.println(person); } } }
练习:使用Lambda标准格式(有参有返回) 题目 给定一个计算器Calculator
接口,内含抽象方法calc
可以将两个int数字相加得到和值:
1 2 3 public interface Calculator { int calc (int a, int b) ; }
在下面的代码中,请使用Lambda的标准格式 调用invokeCalc
方法,完成120和130的相加计算:
1 2 3 4 5 6 7 8 9 10 public class Demo08InvokeCalc { public static void main (String[] args) { } private static void invokeCalc (int a, int b, Calculator calculator) { int result = calculator.calc(a, b); System.out.println("结果是:" + result); } }
解答 1 2 3 4 5 public static void main (String[] args) { invokeCalc(120 , 130 , (int a, int b) -> { return a + b; }); }
备注:小括号代表 Calculator
接口 calc
抽象方法的参数,大括号代表calc
的方法体。
Lambda 省略格式 可推导即可省略 Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以根据上下文推导得知的信息,都可以省略。例如上例还可以使用Lambda的省略写法:
1 2 3 public static void main (String[] args) { invokeCalc(120 , 130 , (a, b) -> a + b); }
省略规则 在Lambda标准格式的基础上,使用省略写法的规则为:
小括号内参数的类型可以省略;
如果小括号内有且仅有一个参 ,则小括号可以省略;
如果大括号内有且仅有一个语句 ,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。
备注:掌握这些省略规则后,请对应地回顾本章开头的多线程案例。
练习:使用 Lambda 省略格式 题目 仍然使用前文含有唯一 makeFood
抽象方法的厨子 Cook
接口,在下面的代码中,请使用 Lambda 的省略格式 调用 invokeCook
方法,打印输出“吃饭啦!”字样:
1 2 3 4 5 6 7 8 9 public class Demo09InvokeCook { public static void main (String[] args) { } private static void invokeCook (Cook cook) { cook.makeFood(); } }
解答 1 2 3 public static void main (String[] args) { invokeCook(() -> System.out.println("吃饭啦!" )); }
Lambda 的使用前提 Lambda 的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
使用 Lambda 必须具有接口,且要求接口中有且仅有一个抽象方法 。 无论是 JDK 内置的 Runnable
、Comparator
接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用 Lambda。
使用 Lambda 必须具有上下文推断 。 也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
备注:有且仅有一个抽象方法的接口,称为“函数式接口 ”。
Java 中的 IO 流