原创

    Java 基础教程【四】

    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(): 把集合中的元素,存储到数组中。

    代码实例演示:

    // java.util.Collection 接口,所有单列集合的最顶层的接口,里边定义了所有单列集合共性的方法,任意的单列集合都可以使用 Collection 接口中的方法
    public static void main(String[] args) {
        // 01-创建集合对象,可以使用多态,只要实现了 Collection 接口的实现类都可以,如 ArrayList、hashSet、LinkedList 等等
        Collection<String> collection = new ArrayList<>();
    //    Collection<String> coll = new ArrayList<>();
        System.out.println(collection);    // 结果:,说明重写了 toString 方法
    
        // 02-添加操作,把给定的对象添加到当前集合中,返回值是一个 boolean 值,一般都返回 true ,所以可以不用接收
        boolean bool01 = collection.add("张三");
        System.out.println(bool01);
        System.out.println(collection);
        collection.add("李四");
        collection.add("李四");
        collection.add("王五");
        collection.add("赵六");
        System.out.println(collection);
    
        // 03-删除操作,把给定的对象在当前集合中删除。返回值是一个 boolean 值,集合中存在元素,删除元素,返回 true,集合中不存在元素,删除失败,返回 false
        boolean bool02 = collection.remove("Jack");
        System.out.println(bool02);    // 元素不存在,返回 false
        boolean bool03 = collection.remove("李四");
        System.out.println(bool03);    // 元素存在,返回 true
        System.out.println(collection);
    
        // 04-是否包含,判断当前集合中是否包含给定的对象。包含返回 true,不包含返回 false
        boolean bool04 = collection.contains("Jack");
        System.out.println(bool04);    // 不包含,返回 false
        boolean bool05 = collection.contains("张三");
        System.out.println(bool05);    // 包含,返回 true
    
        // 05-是否为空,集合为空返回 true,集合不为空返回 false
        boolean isEmpty = collection.isEmpty();
        System.out.println(isEmpty);
    
        // 06-集合大小,返回集合中元素的个数。
        int size = collection.size();
        System.out.println(size);
        System.out.println(collection);
    
        // 07-集合转数组,把集合中的元素,存储到数组中。
        Object[] objects = collection.toArray();
        for (int i = 0; i < objects.length; i++) {
            System.out.println(objects[i]);
        }
    
        // 08-清空集合操作,清空集合中所有的元素。但是不删除集合,集合还存在
        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 迭代集合中元素,使用代码把上述介绍的方法过一遍:

    public static void main(String[] args) {
        // 01-创建集合对象
        Collection<String> collection = new ArrayList<>();
        // 02-往集合添加元素
        collection.add("Lisa");
        collection.add("Jack");
        collection.add("Tom");
        collection.add("Jerry");
    
        // 03-创建迭代器,注意:Iterator<E> 接口也是有泛型的,迭代器的泛型跟着集合走,集合是什么泛型,迭代器就是什么泛型
        Iterator<String> iterator = collection.iterator();
    
        // 04-使用迭代器遍历集合
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    
        // 05-知识拓展:使用 for 遍历集合
        System.out.println("===========");
        // 这个 for 循环有点儿特殊
        for (Iterator<String> it = collection.iterator(); it.hasNext();){
            System.out.println(it.next());
        }
    
        // 06-如果集合中没有元素,那么会抛出异常 Exception in thread "main" java.util.NoSuchElementException
        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 循环

    所有的解释都在注释里面了,请看代码:

    /**
     * 增强 for 循环(foreach) 特点:
     * 1、增强 for 循环:底层使用的也是迭代器,使用 for 循环的格式,简化了迭代器的书写
     * 2、此 fo r循环必须有被遍历的目标,目标只能是 Collection 或者是数组。
     * 3、是 JDK1.5 之后出现的新特性
     * 4、Collection<E>extends Iterable<E>:所有的单列集合都可以使用增强 for
     * 5、 public interface Iterable<T>实现这个接口允许对象成为 "foreach" 语句的目标。
     * 6、格式:
     *         for(集合/数组的数据类型 变量名: 集合名/数组名){
     *             sout(变量名);
     *         }
     */
    public static void main(String[] args) {
        // 01-创建集合
        ArrayList<String> list = new ArrayList<>();
    
        // 02-添加数据
        list.add("abc");
        list.add("def");
        list.add("ghi");
        list.add("jkl");
    
        // 03-增强 for 循环遍历集合
        for (String str : list) {
            System.out.println(str);
        }
    
        // 04-知识拓展:增强 for 遍历数组
        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、避免了类型强转的麻烦。

    通过如下代码体验一下泛型:

    public static void main(String[] args) {
        // 01-不使用泛型案例
        //listDemoFirst();
    
        // 02-使用泛型案例
        listDemoSecond();
    }
    
    
    /**
     * 创建集合对象,使用泛型:
     * 好处:1、避免了类型转换的麻烦,存储的是什么类型,取出的就是什么类型;2、把运行期异常(代码运行之后会抛出的异常),提升到了编译期(写代码的时候会报错)
     * 弊端:泛型是什么类型,只能存储什么类型的数据
     */
    private static void listDemoSecond() {
        // 01-创建集合并添加数据
        ArrayList<String> arrayList = new ArrayList<>();
        arrayList.add("Tom");
        arrayList.add("Lisa");
        arrayList.add("Jack");
    //    arrayList.add(1);    //直接编译不通过,报错信息:add(java.lang.String)in ArrayList cannot be applied to (int)
    
        // 02-迭代器遍历集合
        Iterator<String> it = arrayList.iterator();
        while (it.hasNext()){
            String string = it.next();
            System.out.println(string + " --> " + string.length());
        }
    }
    
    
    /**
     * 创建集合对象,不使用泛型:
     * 好处:集合不使用泛型,默认类型就是 Object 类型,可以存储任意类型的数据
     * 弊端:不安全,会引发异常
     */
    private static void listDemoFirst() {
        // 01-创建集合并添加数据
        ArrayList list = new ArrayList();
        list.add("Jack");
        list.add(37);
    
        // 02-迭代器遍历集合
        Iterator iterator = list.iterator();
        while (iterator.hasNext()){
            // 03-取出的元素是 Object 类型的
            Object object = iterator.next();
            System.out.println(object);
    
            // 04-想要使用 String 类特有的方法,length 获取字符串的长度;不能使用多态 Object obj = "abc";
            // 05-需要向下转型才能使用相关类方法,然而结果抛出 ClassCastException 类型转换异常,不能把 Integer 类型转换为 String 类型,这就是弊端
            String str = (String)object;
            System.out.println(str.length());
        }
    }
    

    tips:泛型是数据类型的一部分,我们将类名与泛型合并一起看做数据类型。

    泛型的定义和使用

    我们在集合中会大量使用到泛型,这里来完整地学习泛型知识。泛型,用来灵活地将数据类型应用到不同的类、方法、接口当中。将数据类型作为参数进行传递。

    有泛型的类

    例如,API 中的 ArrayList 集合:

    // 含有泛型的类的定义格式:修饰符 class 类名<代表泛型的变量> {  }
    class ArrayList<E>{ 
        public boolean add(E e){ }
    
        public E get(int index){ }
        ....
    }
    

    实际代码如下:

    /**
     * 定义一个含有泛型的类,模拟 ArrayList 集合
     * 泛型是一个未知的数据类型,当我们不确定什么什么数据类型的时候,可以使用泛型
     * 泛型可以接收任意的数据类型,可以使用 Integer,String,Student...
     * 创建对象的时候确定泛型的数据类型
     */
    public class MyArrayList<E> {
        private E name;
    
        public E getName() {
            return name;
        }
    
        public void setName(E name) {
            this.name = name;
        }
    }
    

    使用自定义的泛型类: 即什么时候确定泛型。

    public static void main(String[] args) {
        // 01-创建 MyArrayList 对象,不写泛型,默认为 Object 类型
        MyArrayList myArrayList = new MyArrayList();
        myArrayList.setName("Jack");
        Object object = myArrayList.getName();
        System.out.println(object);
    
        //02-创建 MyArrayList 对象,泛型使用 Integer 类型
        MyArrayList<Integer> integerMyArrayList = new MyArrayList<>();
        integerMyArrayList.setName(520);
        System.out.println(integerMyArrayList.getName());
    
        // 03-创建 MyArrayList 对象,泛型使用 String 类型
        MyArrayList<String> stringMyArrayList = new MyArrayList<>();
        stringMyArrayList.setName("Tom");
        System.out.println(stringMyArrayList.getName());
    }
    

    有泛型的方法

    例如:

    // 有泛型的方法的定义格式:修饰符 <代表泛型的变量> 返回值类型 方法名(参数){  }
    public class MyGenericMethod {  
        public <MVP> void show(MVP mvp) {
            System.out.println(mvp.getClass());
        }
        
        public <MVP> MVP show2(MVP mvp) {
            return mvp;
        }
    }
    

    具体定义代码如下:

    /**
     * 定义含有泛型的方法:泛型定义在方法的修饰符和返回值类型之间
     * 格式: 修饰符 <泛型> 返回值类型 方法名(参数列表(使用泛型)){
     *          方法体;
     *       }
     * 含有泛型的方法,在调用方法的时候确定泛型的数据类型 ,传递什么类型的参数,泛型就是什么类
     */
    
    // 01-定义一个含有泛型的方法
    public <M> void method01(M m){
        System.out.println(m);
    }
    
    // 02-定义一个含有泛型的静态方法
    public static <S> void method02(S s){
        System.out.println(s);
    }
    

    使用格式:调用方法时,确定泛型的类型 ,相关代码如下:

    public class GenericMethod {
        public static void main(String[] args) {
            // 01-创建 GenericMethod 对象
            GenericMethod genericMethod = new GenericMethod();
            
            // 02-调用含有泛型的方法 method01,传递什么类型,泛型就是什么类型
            genericMethod.method01(10);
            genericMethod.method01("abc");
            genericMethod.method01(3.14);
            genericMethod.method01(true);
            
            // 03-调用静态方法,通过类名.方法名(参数)可以直接使用
            GenericMethod.method02("静态方法");
            GenericMethod.method02(1.732);
        }
    
        // 01-定义一个含有泛型的方法
        public <M> void method01(M m){
            System.out.println(m);
        }
    
        // 02-定义一个含有泛型的静态方法
        public static <S> void method02(S s){
            System.out.println(s);
        }
    }
    

    有泛型的接口

    定义格式及示例如下:

    // 定义格式:修饰符 interface接口名<代表泛型的变量> {  }
    public interface MyGenericInterface<E>{
        public abstract void add(E e);
        
        public abstract E getE();  
    }
    

    含有泛型的接口的具体代码如下:

    // 定义含有泛型的接口
    public interface GenericInterface<E> {
        public abstract void method(E e);
    }
    

    泛型接口的第一种实现代码如下:

    /**
     * 含有泛型的接口,第一种使用方式:定义接口的实现类,实现接口,指定接口的泛型
     * public interface Iterator<E> {
     *     E next();
     * }
     * Scanner类实现了 Iterator 接口,并指定接口的泛型为 String , 所以重写的 next 方法泛型默认就是 String
     * public final class Scanner implements Iterator<String>{
     *     public String next() {}
     * }
     */
    public class GenericInterfaceImpl1 implements GenericInterface<String>{
        @Override
        public void method(String str) {
            System.out.println(str);
        }
    }
    

    泛型接口的第二种实现代码如下:

    /**
     * 含有泛型的接口第二种使用方式:接口使用什么泛型,实现类就使用什么泛型,类跟着接口走
     * 就相当于定义了一个含有泛型的类,创建对象的时候确定泛型的类型
     * public interface List<E>{
     *     boolean add(E e);
     *     E get(int index);
     * }
     * public class ArrayList<E> implements List<E>{
     *     public boolean add(E e) {}
     *     public E get(int index) {}
     * }
     */
    public class GenericInterfaceImpl2<E> implements GenericInterface<E> {
        @Override
        public void method(E e) {
            System.out.println(e);
        }
    }
    

    测试代码如下:

    // 测试含有泛型的接口
    public class Demo04GenericInterface {
        public static void main(String[] args) {
            // 01-创建 GenericInterfaceImpl1 对象
            GenericInterfaceImpl1 gi1 = new GenericInterfaceImpl1();
            gi1.method("字符串");
    
            // 02-创建 GenericInterfaceImpl2 对象
            GenericInterfaceImpl2<Integer> gi2 = new GenericInterfaceImpl2<>();
            gi2.method(10);
            GenericInterfaceImpl2<Double> gi3 = new GenericInterfaceImpl2<>();
            gi3.method(8.8);
        }
    }
    

    泛型通配符

    当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符 <?> 表示。但是一旦使用泛型的通配符后,只能使用 Object 类中的共性方法,集合中元素自身方法无法使用。

    通配符基本使用

    泛型的通配符:**不知道使用什么类型来接收的时候,此时可以使用 ?, ? 表示未知通配符。**此时只能接受数据,不能往该集合中存储数据。举个例子大家理解使用即可:

    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){}
    
    

    具体实际代码如下:

    public static void main(String[] args) {
        /**
         * 泛型的通配符: ? 代表任意的数据类型
         * 使用方式: 1、不能创建对象使用;2、只能作为方法的参数使用
         */
        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);
    
    }
    
    
    /**
     * 定义一个方法,能遍历所有类型的 ArrayList 集合
     * 这时候我们不知道 ArrayList 集合使用什么数据类型 ,可以泛型的通配符 ? 来接收数据类型
     * 注意: 泛型没有继承概念的
     */
    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 的父类

    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);
    
    }
    // 泛型的上限:此时的泛型?,必须是 Number 类型或者 Number 类型的子类
    public static void getElement1(Collection<? extends Number> coll){}
    
    // 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
    public static void getElement2(Collection<? super Number> coll){}
    

    斗地主案例(单列)

    案例介绍

    按照斗地主的规则,完成洗牌发牌的动作。**具体规则:**使用 54 张牌打乱顺序,三个玩家参与游戏,三人交替摸牌,每人 17 张牌,最后三张留作底牌。

    案例分析

    准备牌:
    牌可以设计为一个 ArrayList ,每个字符串为一张牌。 每张牌由花色数字两部分组成,我们可以使用花色集合与数字集合嵌套迭代完成每张牌的组装。 牌由 Collections 类的 shuffle 方法进行随机排序。
    发牌:
    将每个人以及底牌设计为 ArrayList , 将最后 3 张牌直接存放于底牌,剩余牌通过对 3 取模依次发牌。
    看牌:
    直接打印每个集合。

    代码实现

    public class DouDiZhu {
        // 斗地主综合案例:1、准备牌  2、洗牌  3、发牌  4、看牌
        public static void main(String[] args) {
            // 01-准备牌:定义一个存储 54 张牌的 ArrayList 集合,泛型使用 String
            ArrayList<String> poker = new ArrayList<>();
    
            // 02-定义两个数组,一个数组存储牌的花色,一个数组存储牌的序号
            String[] colors = {"♠","♥","♣","♦"};
            String[] numbers = {"2","A","K","Q","J","10","9","8","7","6","5","4","3"};
    
            // 03-先把大王和小王存储到 poker 集合中
            poker.add("大王");
            poker.add("小王");
    
            // 05-循环嵌套遍历两个数组,组装 52 张牌
            for(String number : numbers){
                for (String color : colors) {
                    // 06-把组装好的牌存储到 poker 集合中
                    poker.add(color + number);
                }
            }
            // 07-洗牌:使用集合的工具类 Collections 中的方法 static void shuffle(List<?> list) 使用默认随机源对指定列表进行置换。
            Collections.shuffle(poker);
    
            // 08-发牌:定义 4 个集合,存储玩家的牌和底牌
            ArrayList<String> player01 = new ArrayList<>();
            ArrayList<String> player02 = new ArrayList<>();
            ArrayList<String> player03 = new ArrayList<>();
            ArrayList<String> diPai = new ArrayList<>();
    
            /**
             * 1、遍历 poker 集合,获取每一张牌
             * 2、使用 poker 集合的索引 % 3 给 3 个玩家轮流发牌
             * 剩余 3 张牌给底牌。注意:先判断底牌(i>=51),否则牌就发没了
             */
            for (int i = 0; i < poker.size() ; i++) {
                // 获取每一张牌
                String p = poker.get(i);
                // 轮流发牌
                if(i >= 51){
                    // 给底牌发牌
                    diPai.add(p);
                }else if(i % 3 == 0){
                    // 给 玩家1 发牌
                    player01.add(p);
                }else if(i % 3 == 1){
                    // 给 玩家2 发牌
                    player02.add(p);
                }else if(i % 3 == 2){
                    // 给 玩家3 发牌
                    player03.add(p);
                }
            }
    
            // 09-看牌
            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集合特有的方法都是跟索引相关,我们已经都学习过,那么我们再来复习一遍吧:

    /*
        java.util.List接口 extends Collection接口
        List接口的特点:
            1.有序的集合,存储元素和取出元素的顺序是一致的(存储123 取出123)
            2.有索引,包含了一些带索引的方法
            3.允许存储重复的元素
    
        List接口中带索引的方法(特有)
            - public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
            - public E get(int index):返回集合中指定位置的元素。
            - public E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
            - public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
        注意:
            操作索引的时候,一定要防止索引越界异常
            IndexOutOfBoundsException:索引越界异常,集合会报
            ArrayIndexOutOfBoundsException:数组索引越界异常
            StringIndexOutOfBoundsException:字符串索引越界异常
     */
    public class Demo01List {
        public static void main(String[] args) {
            //创建一个List集合对象,多态
            List<String> list = new ArrayList<>();
            //使用add方法往集合中添加元素
            list.add("a");
            list.add("b");
            list.add("c");
            list.add("d");
            list.add("a");
            //打印集合
            System.out.println(list);//[a, b, c, d, a]  不是地址重写了toString
    
            //public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
            //在c和d之间添加一个itheima
            list.add(3,"itheima");//[a, b, c, itheima, d, a]
            System.out.println(list);
    
            //public E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
            //移除元素
            String removeE = list.remove(2);
            System.out.println("被移除的元素:"+removeE);//被移除的元素:c
            System.out.println(list);//[a, b, itheima, d, a]
    
            //public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
            //把最后一个a,替换为A
            String setE = list.set(4, "A");
            System.out.println("被替换的元素:"+setE);//被替换的元素:a
            System.out.println(list);//[a, b, itheima, d, A]
    
            //List集合遍历有3种方式
            //使用普通的for循环
            for(int i=0; i<list.size(); i++){
                //public E get(int index):返回集合中指定位置的元素。
                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
            for (String s : list) {
                System.out.println(s);
            }
    
            String r = list.get(5);//IndexOutOfBoundsException: Index 5 out-of-bounds for length 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集合也可以作为堆栈,队列的结构使用。(了解即可)

    方法演示:

    /*
        java.util.LinkedList集合 implements List接口
        LinkedList集合的特点:
            1.底层是一个链表结构:查询慢,增删快
            2.里边包含了大量操作首尾元素的方法
            注意:使用LinkedList集合特有的方法,不能使用多态
    
            - public void addFirst(E e):将指定元素插入此列表的开头。
            - public void addLast(E e):将指定元素添加到此列表的结尾。
            - public void push(E e):将元素推入此列表所表示的堆栈。
    
            - public E getFirst():返回此列表的第一个元素。
            - public E getLast():返回此列表的最后一个元素。
    
            - public E removeFirst():移除并返回此列表的第一个元素。
            - public E removeLast():移除并返回此列表的最后一个元素。
            - public E pop():从此列表所表示的堆栈处弹出一个元素。
    
            - public boolean isEmpty():如果列表不包含元素,则返回true。
    
     */
    public class Demo02LinkedList {
        public static void main(String[] args) {
            show03();
        }
    
        /*
            - public E removeFirst():移除并返回此列表的第一个元素。
            - public E removeLast():移除并返回此列表的最后一个元素。
            - public E pop():从此列表所表示的堆栈处弹出一个元素。此方法相当于 removeFirst
         */
        private static void show03() {
            //创建LinkedList集合对象
            LinkedList<String> linked = new LinkedList<>();
            //使用add方法往集合中添加元素
            linked.add("a");
            linked.add("b");
            linked.add("c");
            System.out.println(linked);//[a, b, c]
    
            //String first = linked.removeFirst();
            String first = linked.pop();
            System.out.println("被移除的第一个元素:"+first);
            String last = linked.removeLast();
            System.out.println("被移除的最后一个元素:"+last);
            System.out.println(linked);//[b]
        }
    
        /*
            - public E getFirst():返回此列表的第一个元素。
            - public E getLast():返回此列表的最后一个元素。
         */
        private static void show02() {
            //创建LinkedList集合对象
            LinkedList<String> linked = new LinkedList<>();
            //使用add方法往集合中添加元素
            linked.add("a");
            linked.add("b");
            linked.add("c");
    
            //linked.clear();//清空集合中的元素 在获取集合中的元素会抛出NoSuchElementException
    
            //public boolean isEmpty():如果列表不包含元素,则返回true。
            if(!linked.isEmpty()){
                String first = linked.getFirst();
                System.out.println(first);//a
                String last = linked.getLast();
                System.out.println(last);//c
            }
        }
    
        /*
            - public void addFirst(E e):将指定元素插入此列表的开头。
            - public void addLast(E e):将指定元素添加到此列表的结尾。
            - public void push(E e):将元素推入此列表所表示的堆栈。此方法等效于 addFirst(E)。
         */
        private static void show01() {
            //创建LinkedList集合对象
            LinkedList<String> linked = new LinkedList<>();
            //使用add方法往集合中添加元素
            linked.add("a");
            linked.add("b");
            linked.add("c");
            System.out.println(linked);//[a, b, c]
    
            //public void addFirst(E e):将指定元素插入此列表的开头。
            //linked.addFirst("www");
            linked.push("www");
            System.out.println(linked);//[www, a, b, c]
    
            //public void addLast(E e):将指定元素添加到此列表的结尾。此方法等效于 add()
            linked.addLast("com");
            System.out.println(linked);//[www, a, b, c, com]
        }
    }
    
    

    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 的代码演示如下:

    /*
        java.util.Map<k,v>集合
        Map集合的特点:
            1.Map集合是一个双列集合,一个元素包含两个值(一个key,一个value)
            2.Map集合中的元素,key和value的数据类型可以相同,也可以不同
            3.Map集合中的元素,key是不允许重复的,value是可以重复的
            4.Map集合中的元素,key和value是一一对应
        java.util.HashMap<k,v>集合 implements Map<k,v>接口
        HashMap集合的特点:
            1.HashMap集合底层是哈希表:查询的速度特别的快
                JDK1.8之前:数组+单向链表
                JDK1.8之后:数组+单向链表|红黑树(链表的长度超过8):提高查询的速度
            2.hashMap集合是一个无序的集合,存储元素和取出元素的顺序有可能不一致
       java.util.LinkedHashMap<k,v>集合 extends HashMap<k,v>集合
       LinkedHashMap的特点:
            1.LinkedHashMap集合底层是哈希表+链表(保证迭代的顺序)
            2.LinkedHashMap集合是一个有序的集合,存储元素和取出元素的顺序是一致的
     */
    public class Demo01Map {
        public static void main(String[] args) {
            show04();
        }
    
        /*
            boolean containsKey(Object key) 判断集合中是否包含指定的键。
            包含返回true,不包含返回false
         */
        private static void show04() {
            //创建Map集合对象
            Map<String,Integer> map = new HashMap<>();
            map.put("赵丽颖",168);
            map.put("杨颖",165);
            map.put("林志玲",178);
    
            boolean b1 = map.containsKey("赵丽颖");
            System.out.println("b1:"+b1);//b1:true
    
            boolean b2 = map.containsKey("赵颖");
            System.out.println("b2:"+b2);//b2:false
        }
    
        /*
            public V get(Object key) 根据指定的键,在Map集合中获取对应的值。
                返回值:
                    key存在,返回对应的value值
                    key不存在,返回null
         */
        private static void show03() {
            //创建Map集合对象
            Map<String,Integer> map = new HashMap<>();
            map.put("赵丽颖",168);
            map.put("杨颖",165);
            map.put("林志玲",178);
    
            Integer v1 = map.get("杨颖");
            System.out.println("v1:"+v1);//v1:165
    
            Integer v2 = map.get("迪丽热巴");
            System.out.println("v2:"+v2);//v2:null
        }
    
        /*
            public V remove(Object key): 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。
                返回值:V
                    key存在,v返回被删除的值
                    key不存在,v返回null
         */
        private static void show02() {
            //创建Map集合对象
            Map<String,Integer> map = new HashMap<>();
            map.put("赵丽颖",168);
            map.put("杨颖",165);
            map.put("林志玲",178);
            System.out.println(map);//{林志玲=178, 赵丽颖=168, 杨颖=165}
    
            Integer v1 = map.remove("林志玲");
            System.out.println("v1:"+v1);//v1:178
    
            System.out.println(map);//{赵丽颖=168, 杨颖=165}
    
            //int v2 = map.remove("林志颖");//自动拆箱  NullPointerException
            Integer v2 = map.remove("林志颖");
            System.out.println("v2:"+v2);//v2:null
    
            System.out.println(map);//{赵丽颖=168, 杨颖=165}
        }
    
        /*
            public V put(K key, V value):  把指定的键与指定的值添加到Map集合中。
                返回值:v
                    存储键值对的时候,key不重复,返回值V是null
                    存储键值对的时候,key重复,会使用新的value替换map中重复的value,返回被替换的value值
         */
        private static void show01() {
            //创建Map集合对象,多态
            Map<String,String> map = new HashMap<>();
    
            String v1 = map.put("李晨", "范冰冰1");
            System.out.println("v1:"+v1);//v1:null
    
            String v2 = map.put("李晨", "范冰冰2");
            System.out.println("v2:"+v2);//v2:范冰冰1
    
            System.out.println(map);//{李晨=范冰冰2}
    
            map.put("冷锋","龙小云");
            map.put("杨过","小龙女");
            map.put("尹志平","小龙女");
            System.out.println(map);//{杨过=小龙女, 尹志平=小龙女, 李晨=范冰冰2, 冷锋=龙小云}
        }
    }
    

    tips:使用put方法时,若指定的键(key)在集合中没有,则没有这个键对应的值,返回null,并把指定的键值添加到集合中;若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值),并把指定键所对应的值,替换成指定的新值。

    Map 遍历

    
    /*
        Map集合的第一种遍历方式:通过键找值的方式
        Map集合中的方法:
             Set<K> keySet() 返回此映射中包含的键的 Set 视图。
        实现步骤:
            1.使用Map集合中的方法keySet(),把Map集合所有的key取出来,存储到一个Set集合中
            2.遍历set集合,获取Map集合中的每一个key
            3.通过Map集合中的方法get(key),通过key找到value
     */
    public class Demo02KeySet {
        public static void main(String[] args) {
            //创建Map集合对象
            Map<String,Integer> map = new HashMap<>();
            map.put("赵丽颖",168);
            map.put("杨颖",165);
            map.put("林志玲",178);
    
            //1.使用Map集合中的方法keySet(),把Map集合所有的key取出来,存储到一个Set集合中
            Set<String> set = map.keySet();
    
            //2.遍历set集合,获取Map集合中的每一个key
            //使用迭代器遍历Set集合
            Iterator<String> it = set.iterator();
            while (it.hasNext()){
                String key = it.next();
                //3.通过Map集合中的方法get(key),通过key找到value
                Integer value = map.get(key);
                System.out.println(key+"="+value);
            }
            System.out.println("-------------------");
            //使用增强for遍历Set集合
            for(String key : set){
                //3.通过Map集合中的方法get(key),通过key找到value
                Integer value = map.get(key);
                System.out.println(key+"="+value);
            }
            System.out.println("-------------------");
            //使用增强for遍历Set集合
            for(String key : map.keySet()){
                //3.通过Map集合中的方法get(key),通过key找到value
                Integer value = map.get(key);
                System.out.println(key+"="+value);
            }
        }
    }
    

    Entry键值对对象

    /*
        Map集合遍历的第二种方式:使用Entry对象遍历
    
        Map集合中的方法:
            Set<Map.Entry<K,V>> entrySet() 返回此映射中包含的映射关系的 Set 视图。
    
        实现步骤:
            1.使用Map集合中的方法entrySet(),把Map集合中多个Entry对象取出来,存储到一个Set集合中
            2.遍历Set集合,获取每一个Entry对象
            3.使用Entry对象中的方法getKey()和getValue()获取键与值
     */
    public class Demo03EntrySet {
        public static void main(String[] args) {
            //创建Map集合对象
            Map<String,Integer> map = new HashMap<>();
            map.put("赵丽颖",168);
            map.put("杨颖",165);
            map.put("林志玲",178);
    
            //1.使用Map集合中的方法entrySet(),把Map集合中多个Entry对象取出来,存储到一个Set集合中
            Set<Map.Entry<String, Integer>> set = map.entrySet();
    
            //2.遍历Set集合,获取每一个Entry对象
            //使用迭代器遍历Set集合
            Iterator<Map.Entry<String, Integer>> it = set.iterator();
            while(it.hasNext()){
                Map.Entry<String, Integer> entry = it.next();
                //3.使用Entry对象中的方法getKey()和getValue()获取键与值
                String key = entry.getKey();
                Integer value = entry.getValue();
                System.out.println(key+"="+value);
            }
            System.out.println("-----------------------");
            for(Map.Entry<String,Integer> entry:set){
                //3.使用Entry对象中的方法getKey()和getValue()获取键与值
                String key = entry.getKey();
                Integer value = entry.getValue();
                System.out.println(key+"="+value);
            }
        }
    }
    
    

    HashMap 存储自定义类型键值

    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;
        }
    }
    
    /*
        HashMap存储自定义类型键值
        Map集合保证key是唯一的:
            作为key的元素,必须重写hashCode方法和equals方法,以保证key唯一
     */
    public class Demo01HashMapSavePerson {
        public static void main(String[] args) {
            show02();
        }
    
        /*
            HashMap存储自定义类型键值
            key:Person类型
                Person类就必须重写hashCode方法和equals方法,以保证key唯一
            value:String类型
                可以重复
         */
        private static void show02() {
            //创建HashMap集合
            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),"毛里求斯");
            //使用entrySet和增强for遍历Map集合
            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);
            }
        }
    
        /*
            HashMap存储自定义类型键值
            key:String类型
                String类重写hashCode方法和equals方法,可以保证key唯一
            value:Person类型
                value可以重复(同名同年龄的人视为同一个)
         */
        private static void show01() {
            //创建HashMap集合
            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));
            //使用keySet加增强for遍历Map集合
            Set<String> set = map.keySet();
            for (String key : set) {
                Person value = map.get(key);
                System.out.println(key+"-->"+value);
            }
        }
    }
    

    LinkedHashMap

    我们知道HashMap保证成对元素唯一,并且查询速度很快,可是成对元素存放进去是没有顺序的,那么我们要保证有序,还要速度快怎么办呢?在HashMap下面有一个子类LinkedHashMap,它是链表和哈希表组合的一个数据存储结构。

    /*
        java.util.LinkedHashMap<K,V> entends HashMap<K,V>
        Map 接口的哈希表和链接列表实现,具有可预知的迭代顺序。
        底层原理:
            哈希表+链表(记录元素的顺序)
     */
    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);// key不允许重复,无序 {a=d, b=b, c=c}
    
            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);// key不允许重复,有序 {a=d, c=c, b=b}
        }
    }
    
    /*
        java.util.Hashtable<K,V>集合 implements Map<K,V>接口
    
        Hashtable:底层也是一个哈希表,是一个线程安全的集合,是单线程集合,速度慢
        HashMap:底层是一个哈希表,是一个线程不安全的集合,是多线程的集合,速度快
    
        HashMap集合(之前学的所有的集合):可以存储null值,null键
        Hashtable集合,不能存储null值,null键
    
        Hashtable和Vector集合一样,在jdk1.2版本之后被更先进的集合(HashMap,ArrayList)取代了
        Hashtable的子类Properties依然活跃在历史舞台
        Properties集合是一个唯一和IO流相结合的集合
     */
    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);//{null=null, b=null}
    
            Hashtable<String,String> table = new Hashtable<>();
            //table.put(null,"a");//NullPointerException
            //table.put("b",null);//NullPointerException
            table.put(null,null);//NullPointerException
        }
    }
    
    
    

    Map集合练习

    /*
        练习:
            计算一个字符串中每个字符出现次数
    
        分析:
            1.使用Scanner获取用户输入的字符串
            2.创建Map集合,key是字符串中的字符,value是字符的个数
            3.遍历字符串,获取每一个字符
            4.使用获取到的字符,去Map集合判断key是否存在
                key存在:
                    通过字符(key),获取value(字符个数)
                    value++
                    put(key,value)把新的value存储到Map集合中
                key不存在:
                    put(key,1)
            5.遍历Map集合,输出结果
     */
    public class Demo03MapTest {
        public static void main(String[] args) {
            //1.使用Scanner获取用户输入的字符串
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入一个字符串:");
            String str = sc.next();
            //2.创建Map集合,key是字符串中的字符,value是字符的个数
            HashMap<Character,Integer> map = new HashMap<>();
            //3.遍历字符串,获取每一个字符
            for(char c :str.toCharArray()){
                //4.使用获取到的字符,去Map集合判断key是否存在
                if(map.containsKey(c)){
                    //key存在
                    Integer value = map.get(c);
                    value++;
                    map.put(c,value);
                }else{
                    //key不存在
                    map.put(c,1);
                }
            }
            //5.遍历Map集合,输出结果
            for(Character key :map.keySet()){
                Integer value = map.get(key);
                System.out.println(key+"="+value);
            }
        }
    }
    
    

    JDK9 新特性

    /*
        JDK9的新特性:
            List接口,Set接口,Map接口:里边增加了一个静态的方法of,可以给集合一次性添加多个元素
            static <E> List<E> of​(E... elements)
            使用前提:
                当集合中存储的元素的个数已经确定了,不在改变时使用
         注意:
            1.of方法只适用于List接口,Set接口,Map接口,不适用于接接口的实现类
            2.of方法的返回值是一个不能改变的集合,集合不能再使用add,put方法添加元素,会抛出异常
            3.Set接口和Map接口在调用of方法的时候,不能有重复的元素,否则会抛出异常
     */
    public class Demo01JDK9 {
        public static void main(String[] args) {
            List<String> list = List.of("a", "b", "a", "c", "d");
            System.out.println(list);//[a, b, a, c, d]
            //list.add("w");//UnsupportedOperationException:不支持操作异常
    
            //Set<String> set = Set.of("a", "b", "a", "c", "d");//IllegalArgumentException:非法参数异常,有重复的元素
            Set<String> set = Set.of("a", "b", "c", "d");
            System.out.println(set);
            //set.add("w");//UnsupportedOperationException:不支持操作异常
    
            //Map<String, Integer> map = Map.of("张三", 18, "李四", 19, "王五", 20,"张三",19);////IllegalArgumentException:非法参数异常,有重复的元素
            Map<String, Integer> map = Map.of("张三", 18, "李四", 19, "王五", 20);
            System.out.println(map);//{王五=20, 李四=19, 张三=18}
            //map.put("赵四",30);//UnsupportedOperationException:不支持操作异常
        }
    }
    

    Debug 调试

    /*
        Debug调试程序:
            可以让代码逐行执行,查看代码执行的过程,调试程序中出现的bug
        使用方式:
            在行号的右边,鼠标左键单击,添加断点(每个方法的第一行,哪里有bug添加到哪里)
            右键,选择Debug执行程序
            程序就会停留在添加的第一个断点处
        执行程序:
            f8:逐行执行程序
            f7:进入到方法中
            shift+f8:跳出方法
            f9:跳到下一个断点,如果没有下一个断点,那么就结束程序
            ctrl+f2:退出debug模式,停止程序
            Console:切换到控制台
     */
    public class Demo01Debug {
        public static void main(String[] args) {
            /*int a = 10;
            int b = 20;
            int sum = a + b;
            System.out.println(sum);*/
    
            /*for (int i = 0; i <3 ; i++) {
                System.out.println(i);
            }*/
    
            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.看牌
     */
    public class DouDiZhu {
        public static void main(String[] args) {
            //1.准备牌
            //创建一个Map集合,存储牌的索引和组装好的牌
            HashMap<Integer,String> poker = new HashMap<>();
            //创建一个List集合,存储牌的索引
            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++;
            //循环嵌套遍历两个集合,组装52张牌,存储到集合中
            for (String number : numbers) {
                for (String color : colors) {
                    poker.put(index,color+number);
                    pokerIndex.add(index);
                    index++;
                }
            }
            //System.out.println(poker);
            //System.out.println(pokerIndex);
    
            /*
                2.洗牌
                使用Collections中的方法shuffle(List)
             */
            Collections.shuffle(pokerIndex);
            //System.out.println(pokerIndex);
    
            /*
                3.发牌
             */
            //定义4个集合,存储玩家牌的索引,和底牌的索引
            ArrayList<Integer> player01 = new ArrayList<>();
            ArrayList<Integer> player02 = new ArrayList<>();
            ArrayList<Integer> player03 = new ArrayList<>();
            ArrayList<Integer> diPai = new ArrayList<>();
            //遍历存储牌索引的List集合,获取每一个牌的索引
            for (int i = 0; i <pokerIndex.size() ; i++) {
                Integer in = pokerIndex.get(i);
                //先判断底牌
                if(i>=51){
                    //给底牌发牌
                    diPai.add(in);
                }else if(i%3==0){
                    //给玩家1发牌
                    player01.add(in);
                }else if(i%3==1){
                    //给玩家2发牌
                    player02.add(in);
                }else if(i%3==2){
                    //给玩家3发牌
                    player03.add(in);
                }
            }
    
            /*
                4.排序
                使用Collections中的方法sort(List)
                默认是升序排序
             */
            Collections.sort(player01);
            Collections.sort(player02);
            Collections.sort(player03);
            Collections.sort(diPai);
    
            /*
                5.看牌
                调用看牌的方法
             */
            lookPoker("刘德华",poker,player01);
            lookPoker("周润发",poker,player02);
            lookPoker("周星驰",poker,player03);
            lookPoker("底牌",poker,diPai);
        }
    
        /*
            定义一个看牌的方法,提高代码的复用性
            参数:
                String name:玩家名称
                HashMap<Integer,String> poker:存储牌的poker集合
                ArrayList<Integer> list:存储玩家和底牌的List集合
            查表法:
                 遍历玩家或者底牌集合,获取牌的索引
                 使用牌的索引,去Map集合中,找到对应的牌
         */
        public static void lookPoker(String name,HashMap<Integer,String> poker,ArrayList<Integer> list){
            //输出玩家名称,不换行
            System.out.print(name+":");
            //遍历玩家或者底牌集合,获取牌的索引
            for (Integer key : list) {
                //使用牌的索引,去Map集合中,找到对应的牌
                String value = poker.get(key);
                System.out.print(value+" ");
            }
            System.out.println();//打印完每一个玩家的牌,换行
        }
    
    }
    

    异常处理

    异常概念

    异常 :就是不正常的意思,指的是程序在执行过程中,出现的非正常的情况,最终会导致 JVM 的非正常停止。在 Java 等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java 处理异常的方式是中断处理。

    异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行。

    异常体系

    异常机制其实是帮助我们找到程序中的问题,异常的根类是 java.lang.Throwable ,其下有两个子类:java.lang.Errorjava.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、需要将这个异常对象告知给调用者。怎么告知呢?怎么将这个异常对象传递到调用者处呢?通过关键字 throw 就可以完成。throw 异常对象。throw 用在方法内 ,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。

    使用格式:

    throw new 异常类名(参数);
    

    例如:

    throw new NullPointerException("要访问的arr数组不存在");
    
    throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");
    

    学习完抛出异常的格式后,我们通过下面程序演示下 throw 的使用。

    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 抛出异常对象后,方法已经无法继续运算。这时就会结束当前方法的执行,并将异常告知给调用者。这时就需要通过异常来解决。 
                 throw new ArrayIndexOutOfBoundsException("哥们,角标越界了~~~");
            }
            int element = arr[index];
            return element;
        }
    }
    

    注意:如果产生了问题,我们就会 throw 将问题描述类即异常进行抛出,也就是将问题返回给该方法的调用者。

    那么对于调用者来说,该怎么处理呢?一种是进行捕获处理,另一种就是继续讲问题声明出去,使用 throws 声明处理。

    声明异常 throws

    声明异常:将问题标识出来,报告给调用者。如果方法内通过 throw 抛出了编译时异常,而没有捕获处理(下面讲解该方式),那么必须通过 throws 进行声明,让调用者去处理。关键字 throws 运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常).

    声明异常格式:

    修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{   }    
    

    声明异常的代码演示:

    public class ThrowsDemo {
        public static void main(String[] args) throws FileNotFoundException {
            read("a.txt");
        }
    
        // 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
        public static void read(String path) throws FileNotFoundException {
            if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
                // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw
                throw new FileNotFoundException("文件不存在");
            }
        }
    }
    

    throws用于进行异常类的声明,若该方法可能有多种异常情况产生,那么在throws后面可以写多个异常类,用逗号隔开。

    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")) {    // 如果不是 a.txt这个文件 
                // 如果不是 a.txt ,认为该文件不存在,是一个异常 throw
                throw new FileNotFoundException("文件不存在");
            }
            if (!path.equals("b.txt")) {
                throw new IOException();
            }
        }
    }
    

    捕获异常 try_catch

    如果异常出现的话,会立刻终止程序,所以我们得处理异常:

    • 1、该方法不处理,而是声明抛出,由该方法的调用者来处理(throws)。
    • 2、如果要处理,那就在方法中使用 try-catch 的语句块来捕获异常。

    try-catch 的方式就是捕获异常【Java 中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理】,语法如下:

    try{
         编写可能会出现异常的代码
    }catch(异常类型  e){
         处理异常的代码
         // 记录日志/打印异常信息/继续抛出异常
    }
    

    **try:**在该代码块中,编写可能产生异常的代码。 **catch:**用来进行某种异常的捕获,实现对捕获到的异常进行处理。 注意 : try 和 catch 都不能单独使用,必须连用。

    演示如下:

    public class TryCatchDemo {
        public static void main(String[] args) {
            try {    // 当产生异常时,必须有处理方式。要么捕获,要么声明。
                read("b.txt");
            } catch (FileNotFoundException e) {    // 括号中需要定义什么呢?
                // try 中抛出的是什么异常,在括号中就定义什么异常类型
                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 代码参考如下:

    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. 多个异常一次捕获一次处理。

    一般我们是使用一次捕获多次处理方式,格式如下:

    try{
         // 编写可能会出现异常的代码
    }catch(异常类型A  e){    // 当try中出现A类型异常,就用该catch来捕获.
         // 处理异常的代码
         // 记录日志/打印异常信息/继续抛出异常
    }catch(异常类型B  e){    // 当try中出现B类型异常,就用该catch来捕获.
         处理异常的代码
         // 记录日志/打印异常信息/继续抛出异常
    }
    

    注意事项:

    1、这种异常处理方式,要求多个 catch 中的异常不能相同,并且若 catch 中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的 catch 处理,父类异常在下面的 catch 处理。 2、运行时异常被抛出可以不处理。即不捕获也不声明抛出。 3、如果 finally 有 return 语句,永远返回 finally 中的结果,避免该情况。 4、如果父类抛出了多个异常,子类重写父类方法时,抛出和父类相同的异常或者是父类异常的子类或者不抛出异常。 5、父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出。

    自定义异常

    自定义异常概述

    为什么需要自定义异常类:

    我们说了Java中不同的异常类,分别表示着某一种具体的异常情况,那么在开发中总是有些异常情况是SUN没有定义好的,此时我们根据自己业务的异常情况来定义异常类。例如年龄负数问题,考试成绩负数问题等等。

    在上述代码中,发现这些异常都是JDK内部定义好的,但是实际开发中也会出现很多异常,这些异常很可能在JDK中没有定义过,例如年龄负数问题,考试成绩负数问题.那么能不能自己定义异常呢?

    什么是自定义异常类:

    在开发中根据自己业务的异常情况来定义异常类.

    自定义一个业务逻辑异常: RegisterException。一个注册异常类。

    异常类如何定义:

    1. 自定义一个编译期异常: 自定义类并继承于 java.lang.Exception
    2. 自定义一个运行时期的异常类:自定义类 并继承于 java.lang.RuntimeException

    自定义异常的练习

    要求:我们模拟注册操作,如果用户名已存在,则抛出异常并提示:亲,该用户名已经被注册。

    首先定义一个登陆异常类 RegisterException:

    // 业务逻辑异常
    public class RegisterException extends Exception {
        // 空参构造
        public RegisterException() {
        }
    
        // @param message 表示异常提示
        public RegisterException(String message) {
            super(message);
        }
    }
    

    模拟登陆操作,使用数组模拟数据库中存储的数据,并提供当前注册账号是否存在方法用于判断。

    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时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

    线程与进程

    • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

    • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

      简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

    线程调度:

    • 分时调度

      所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

    • 抢占式调度

      优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

      • 设置线程的优先级

      • 抢占式调度详解

        大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

        实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

    多线程原理

    常用方法

    设置线程的名称

    多线程休眠方法有点意思,每隔一秒执行,打印一分钟

    Runnable接口创建线程对象

    Runnable接口创建线程对象和thread创建对象的区别

    线程安全

    买票重复,出现-1

    解决线程安全的三种方法

    线程池 线程池介绍 涉及到一个概念,面向接口编程,就是返回对象是接口类型,不用管接口的实现类

    concurrent executors

    Lambda 表达式

    函数式编程思想概述

    在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做

    面向对象的思想: 做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情.

    函数式编程思想: 只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程

    冗余的 Runnable 代码

    传统写法

    当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable接口来定义任务内容,并使用java.lang.Thread类来启动该线程。代码如下:

    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表达式达到等效:

    public class Demo02LambdaRunnable {
        public static void main(String[] args) {
            new Thread(() -> System.out.println("多线程任务执行!")).start(); // 启动线程
        }
    }
    

    这段代码和刚才的执行效果是完全一样的,可以在1.8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。

    不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担,就是这么简单!

    回顾匿名内部类

    Lambda是怎样击败面向对象的?在上例中,核心代码其实只是如下所示的内容:

    () -> System.out.println("多线程任务执行!")
    

    为了理解Lambda的语义,我们需要从传统的代码起步。

    使用实现类

    要启动一个线程,需要创建一个Thread类的对象并调用start方法。而为了指定线程执行的内容,需要调用Thread类的构造方法:

    • public Thread(Runnable target)

    为了获取Runnable接口的实现对象,可以为该接口定义一个实现类RunnableImpl

    public class RunnableImpl implements Runnable {
        @Override
        public void run() {
            System.out.println("多线程任务执行!");
        }
    }
    

    然后创建该实现类的对象作为Thread类的构造参数:

    public class Demo03ThreadInitParam {
        public static void main(String[] args) {
            Runnable task = new RunnableImpl();
            new Thread(task).start();
        }
    }
    

    使用匿名内部类

    这个RunnableImpl类只是为了实现Runnable接口而存在的,而且仅被使用了唯一一次,所以使用匿名内部类的语法即可省去该类的单独定义,即匿名内部类:

    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语法中,要更加简单:

    () -> System.out.println("多线程任务执行!")
    
    • 前面的一对小括号即run方法的参数(无),代表不需要任何条件;
    • 中间的一个箭头代表将前面的参数传递给后面的代码;
    • 后面的输出语句即业务逻辑代码。

    Lambda标准格式

    Lambda省去面向对象的条条框框,格式由3个部分组成:

    • 一些参数
    • 一个箭头
    • 一段代码

    Lambda表达式的标准格式为:

    (参数类型 参数名称) -> { 代码语句 }
    

    格式说明:

    • 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
    • ->是新引入的语法格式,代表指向动作。
    • 大括号内的语法与传统方法体要求基本一致。

    练习:使用Lambda标准格式(无参无返回)

    题目

    给定一个厨子Cook接口,内含唯一的抽象方法makeFood,且无参数、无返回值。如下:

    public interface Cook {
        void makeFood();
    }
    

    在下面的代码中,请使用Lambda的标准格式调用invokeCook方法,打印输出“吃饭啦!”字样:

    public class Demo05InvokeCook {
        public static void main(String[] args) {
            // TODO 请在此使用Lambda【标准格式】调用invokeCook方法
        }
    
        private static void invokeCook(Cook cook) {
            cook.makeFood();
        }
    }
    

    解答

    public static void main(String[] args) {
        invokeCook(() -> {
              System.out.println("吃饭啦!");
        });
    }
    

    备注:小括号代表Cook接口makeFood抽象方法的参数为空,大括号代表makeFood的方法体。

    Lambda的参数和返回值

    需求:
        使用数组存储多个Person对象
        对数组中的Person对象使用Arrays的sort方法通过年龄进行升序排序
    

    下面举例演示java.util.Comparator<T>接口的使用场景代码,其中的抽象方法定义为:

    • public abstract int compare(T o1, T o2);

    当需要对一个对象数组进行排序时,Arrays.sort方法需要一个Comparator接口实例来指定排序的规则。假设有一个Person类,含有String nameint age两个成员变量:

    public class Person { 
        private String name;
        private int age;
        
        // 省略构造器、toString方法与Getter Setter 
    }
    

    传统写法

    如果使用传统的代码对Person[]数组进行排序,写法如下:

    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); // 第二个参数为排序规则,即Comparator接口实例
    
            for (Person person : array) {
                System.out.println(person);
            }
        }
    }
    

    这种做法在面向对象的思想中,似乎也是“理所当然”的。其中Comparator接口的实例(使用了匿名内部类)代表了“按照年龄从小到大”的排序规则。

    代码分析

    下面我们来搞清楚上述代码真正要做什么事情。

    • 为了排序,Arrays.sort方法需要排序规则,即Comparator接口的实例,抽象方法compare是关键;
    • 为了指定compare的方法体,不得不需要Comparator接口的实现类;
    • 为了省去定义一个ComparatorImpl实现类的麻烦,不得不使用匿名内部类;
    • 必须覆盖重写抽象compare方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
    • 实际上,只有参数和方法体才是关键

    Lambda写法

    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数字相加得到和值:

    public interface Calculator {
        int calc(int a, int b);
    }
    

    在下面的代码中,请使用Lambda的标准格式调用invokeCalc方法,完成120和130的相加计算:

    public class Demo08InvokeCalc {
        public static void main(String[] args) {
            // TODO 请在此使用Lambda【标准格式】调用invokeCalc方法来计算120+130的结果ß
        }
    
        private static void invokeCalc(int a, int b, Calculator calculator) {
            int result = calculator.calc(a, b);
            System.out.println("结果是:" + result);
        }
    }
    

    解答

    public static void main(String[] args) {
        invokeCalc(120, 130, (int a, int b) -> {
              return a + b;
        });
    }
    

    备注:小括号代表 Calculator 接口 calc 抽象方法的参数,大括号代表calc的方法体。

    Lambda 省略格式

    可推导即可省略

    Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以根据上下文推导得知的信息,都可以省略。例如上例还可以使用Lambda的省略写法:

    public static void main(String[] args) {
          invokeCalc(120, 130, (a, b) -> a + b);
    }
    

    省略规则

    在Lambda标准格式的基础上,使用省略写法的规则为:

    1. 小括号内参数的类型可以省略;
    2. 如果小括号内有且仅有一个参,则小括号可以省略;
    3. 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。

    备注:掌握这些省略规则后,请对应地回顾本章开头的多线程案例。

    练习:使用 Lambda 省略格式

    题目

    仍然使用前文含有唯一 makeFood 抽象方法的厨子 Cook 接口,在下面的代码中,请使用 Lambda 的省略格式调用 invokeCook 方法,打印输出“吃饭啦!”字样:

    public class Demo09InvokeCook {
        public static void main(String[] args) {
            // TODO 请在此使用Lambda【省略格式】调用invokeCook方法
        }
    
        private static void invokeCook(Cook cook) {
            cook.makeFood();
        }
    }
    

    解答

    public static void main(String[] args) {
        invokeCook(() -> System.out.println("吃饭啦!"));
    }
    

    Lambda 的使用前提

    Lambda 的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:

    1. 使用 Lambda 必须具有接口,且要求接口中有且仅有一个抽象方法。 无论是 JDK 内置的 RunnableComparator 接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用 Lambda。
    2. 使用 Lambda 必须具有上下文推断。 也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。

    备注:有且仅有一个抽象方法的接口,称为“函数式接口”。

    Java 中的 IO 流

    IO 流知识
    由于本篇过长,IO 流独立成篇:Java 中的 IO 流
    Java
    • 文章作者:GuoShiZhan
    • 创建时间:2021-08-16 11:32:11
    • 更新时间:2021-08-16 11:32:11
    • 版权声明:本文为博主原创文章,未经博主允许不得转载!
    请 在 评 论 区 留 言 哦 ~~~
    1024