JavaNote

Java Note

Java基础

  • 类的五大成员:
    1. 属性
    2. 方法
    3. 构造器
    4. 代码块
    5. 内部类
      • 定义在外部类局部位置上(比如方法内)

        1. 局部内部类(有类名)

          • 可以直接访问外部类的所有成员,包含私有的

          • 不能添加访问修饰符,但是可以使用final修饰

          • 作用域:仅仅在定义它的方法或代码块中

          • 局部内部类可以直接访问外部类的成员

          • 外部类在方法中可以创建内部类对象,然后调用方法即可

          public class InnerClass {
              public static void main(String[] args) {
                  Outer outer = new Outer();
                  outer.m2();
              }
          }
          
          class Outer {
              public int n1 = 100;
              private void m1() {
                  System.out.println("Outer m1()");
              }
          
              public void m2() {
                  // 创建内部类,本质也是一个类
                  final class Inner {
                      private int n1 = 800;
                      public void f1() {
                          // 访问外部类的成员(属性和方法)
                          System.out.println("n1 = " + n1);  // n1 = 800
                          System.out.println("n1 = " + Outer.this.n1);  // n1 = 100
                          m1();
                      }
                  }
                  // 创建内部类对象,并调用其中的方法
                  Inner inner = new Inner();
                  inner.f1();
              }
          }
          
          • 如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员。使用 外部类名.this.成员) 去访问
        2. 匿名内部类(没有类名,重点)AnonymousInnerClass

          • 语法(基于接口的和基于类的匿名内部类)

            new 接口或类(参数列表) {
                类体
            };
            
          • 可以使用内部类简化开发,对于只使用一次的类可以使用匿名内部类

          • jdk底层会分配类名:外部类$1

            class Outer$1 implements/extend IA/FC {
                类体
            }
            
      • 定义在外部类的成员位置上:

        1. 成员内部类(没有static修饰)

          • 定义在外部类的成员位置上
          • 可以直接访问外部类的所有成员,包含私有的
          • 可以添加任意访问修饰符
          • 作用域:同其他成员,为整个类体
          • 如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员。使用 外部类名.this.成员) 去访问
          public class MemberInnerClass {
          
              public static void main(String[] args) {
                  // 方式1:其他类中创建外部类,通过外部类创建对应的内部类对象
                  Outer1 outer1 = new Outer1();
                  Outer1.Inner1 inner1 = outer1.new Inner1();
                  inner1.say();
          
                  // 方式2:在外部类中编写一个方法,可以返回Inner1对象
                  Outer1.Inner1 inner1Instance = outer1.getInner1Instance();
                  inner1Instance.say();
              }
          }
          
          class Outer1 {
              private int n1 = 100;
              private String name = "张三";
          
              // 内部类
              class Inner1 {
                  private double sal = 99.8;
                  public void say() {
                      System.out.println("n1 = " + n1 + ", name = " + name);
                  }
              }
          
              // 编写方法,返回Inner1对象
              public Inner1 getInner1Instance() {
                  return new Inner1();
              }
          }
          
        2. 静态内部类(有static修饰)

          • 放在外部类的成员位置

          • 使用static修饰

          • 可以直接访问外部类的所有成员,包含私有的

          • 可以添加任意访问修饰符

          • 作用域:同其他成员,为整个类体

            public class StaticInnerClass {
            
                public static void main(String[] args) {
                    // 通过外部类创建内部类实例
                    Outer2.Inner2 inner2 = new Outer2.Inner2();
                    inner2.say();
            
                    System.out.println("============");
            
                    // 直接使用static内部类
                    // 通过外部类中的getInner2Instance返回创建的内部类实例
                    Outer2.getInner2Instance().say();
                }
            }
            
            class Outer2 {
            
                private int n1 = 10;
                private static String name = "honvin";
                static class Inner2 {
                    public void say() {
                        System.out.println("name = " + name);
                    }
                }
            
                public static Inner2 getInner2Instance() {
                    return new Inner2();
                }
            }
            

==和equals()

==操作符专门用来比较变量的值是否相同。

  • 如果比较的对象是基本数据类型,则比较数值是否相等;
  • 如果比较的是引用数据类型,则比较的是对象的内存地址是否相等。

equals():常用来比较对象的内容是否相同。

​ equals()方法存在于Object类中,而Object类是所有类的直接或间接父类。

public class JavaTest {
    @Test
    public void TestEquals() {
        String str1 = new String("Hello");
        String str2 = new String("Hello");
        System.out.println(str1 == str2);  // false
        System.out.println(str1.equals(str2));  // true
    }
}

两个字符串比较,必须总是使用equals()方法。

要忽略大小写比较,使用equalsIgnoreCase()方法。

finalize(面试 )

  • 当一些资源被回收时,系统会自动调用该对象的finalize方法,子类可以重写该方法。

  • 什么时候被回收:当一个对象没有任何引用时,jvm会认为这个对象是一个垃圾对象,就会使用垃圾回收机制来销毁该对象,在销毁对象前,会调用finalize方法。

  • 垃圾回收机制的调用是由系统决定的(既有自己的GC算法),也可以通过System.gc()主动出发垃圾回收机制。

静态变量(类变量)static

  1. 静态变量被对象共享。

JDK8 以前放在方法区的静态域

JDK8 以后放在堆中

  1. static类变量,在类加载的时候就生成了。
  2. 类方法(静态方法)中只能访问静态变量静态方法
  3. 非静态方法既可以访问非静态成员也可以访问静态成员。

main方法

``public static void main(String[] args) {}`

  1. main方法是java虚拟机调用,访问权限必须是public

  2. java虚拟机在执行main()方法时不必创建对象,所以必须是static

  3. 该方法接受String类型的数组参数,在执行时传入:

    java xxx(执行程序) 参数1 参数2 参数3
    

    image-20231021200700341

代码块

  1. 没有修饰符的为普通代码块,有static修饰的为静态代码块。

static代码块作用是对类进行初始化,随着类的加载而执行,且只会执行一次。而普通的代码块,每创建一个对象就执行。

  • 类什么时候被加载?
    1. 创建对象实例时(new)
    2. 创建子类对象实例,父类也会被加载
    3. 使用类的静态成员时(静态属性,静态方法)
  1. 创建一个对象时,在一个类中的调用顺序:(重点,难点)

    1. 调用静态代码块和静态属性初始化(注意:静态代码块和静态属性初始化调用的优先级一样,如果有多个静态代码块和多个静态变量初始化,则按他们定义的顺序调用)
    2. 调用普通代码块和普通属性的初始化(注意:普通代码块和普通属性初始化调用的优先级一样,如果有多个普通代码块和多个普通属性初始化,则按定义顺序调用)
    3. 调用构造方法
  2. 创建一个子类对象时(继承关系),他们的静态代码块,静态属性初始化,普通代码块,普通属性初始化,构造方法的调用顺序如下(面试):

    1. 父类的静态代码块和静态属性(优先级一样,按定义顺序执行)
    2. 子类的静态代码块和静态属性(优先级一样,按定义顺序执行)
    3. 父类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
    4. 父类的构造方法
    5. 子类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
    6. 子类的构造方法

枚举类

  1. 使用关键字 enum 代替class
  2. 语法:常量名(实参列表)
  3. 如果有多个常量,使用','分隔
  4. 如果使用 enum 实现枚举,要求将定义常量对象写到最前面
public class Enumeration1 {

    public static void main(String[] args) {
        System.out.println(Season1.SPRING);
    }

}

// 使用 enum 关键字实现枚举类
enum Season1 {

    /***
     * 1、使用关键字 enum 代替class
     * 2、语法:常量名(实参列表)
     * 3、如果有多个常量,使用‘,’分隔
     * 4、如果使用 enum 实现枚举,要求将定义常量对象写到最前面
     */

    SPRING("春天", "温暖"), SUMMER("夏天", "炎热"), AUTUMN("秋天", "凉爽"), WINTER("冬天", "寒冷");
    private String name;
    private String desc;

    Season1(String name, String desc) {
        this.name = name;
        this.desc = desc;
    }

    @Override
    public String toString() {
        return "Season1{" +
                "name='" + name + '\'' +
                ", desc='" + desc + '\'' +
                '}';
    }
}

单继承Object

问题:我们都知道java是单继承模式,所有的类有且只能有一个父类,既然如此为什么继承了Object的类还能继承其它类呢?
举例
Parent继承Object;
Child继承Object;
Child继承Parent;

我们以为Child的继承关系是这样:
在这里插入图片描述
其实它是这样的:
在这里插入图片描述
也就是说Object其实变成了Child的爷爷类

参考:Java所有的类都继承Object为什么还能继承其它类

注解(Annotation)

使用Annotation时要在其前面增加@符号,并把 Annotation 当成一个修饰符使用。

  • 三个基本能的 Annotation:

    1. @Override:限定某个方法,是重写父类方法,该注解只能用于方法

    2. @Deprecated:用于表示某个程序元素(类,方法等)已过时

      • 不再推荐使用,但是还能使用
      • 做到新旧版本的兼容和过度
    3. @SuppressWarnings:抑制编译器警告

      当我们不希望看到警告时,可以在{""}中,写入希望一直的警告信息

      public class SuppressWarnings {
      
          @SuppressWarnings({"all"})
          public static void main(String[] args) {
              List list = new ArrayList();
              list.add("tom");
              list.add("jerry");
              int n;
              System.out.println(list.get(1));
          }
      }
      

      还可以根据下表格填写:

      关键字用途
      all抑制所有警告
      boxing抑制装箱、拆箱操作时候的警告
      cast抑制映射相关的警告
      dep-ann抑制启用注释的警告
      deprecation抑制过期方法警告
      fallthrough抑制在 switch 中缺失 breaks 的警告
      finally抑制 finally 模块没有返回的警告
      hiding抑制相对于隐藏变量的局部变量的警告
      incomplete-switch忽略不完整的 switch 语句
      nls忽略非 nls 格式的字符
      null忽略对 null 的操作
      rawtypes使用 generics 时忽略没有指定相应的类型
      restriction抑制禁止使用劝阻或禁止引用的警告
      serial忽略在 serializable 类中没有声明 serialVersionUID 变量
      static-access抑制不正确的静态访问方式警告
      synthetic-access抑制子类没有按最优方法访问内部类的警告
      unchecked抑制没有进行类型检查操作的警告
      unqualified-field-access抑制没有权限访问的域的警告
      unused抑制没被使用过的代码的警告
  • 元注解:修饰注解的注解

异常

异常介绍

基本概念:Java语言中,将程序执行中发生的不正常情况称为“异常”。

注意:开发过程中的语法错误和逻辑错误不是异常。

  • 执行过程中所发生的异常事件可分为两大类:
    1. Error:Java虚拟机无法解决的严重问题。程序会崩溃。(栈溢出、JVM系统内部错误等)
    2. Exception:因编译错误或偶然的外在因素导致的一般性问题。
      1. 运行时异常
      2. 编译时异常
  • 异常体系图(体现了继承和实现的关系)

异常体系图

  • 常见的运行时异常
    1. NullPointerException:空指针异常。当应用程序试图在需要对象的地方使用 null 时,抛出该异常;
    2. ArithmeticException:数学运算异常;
    3. ArrayIndexOutOfBoundsException:数组下标越界异常;
    4. ClassCastException:类型转换异常;
    5. NumberFormatException:数字格式不正确异常
  • 编译异常
    1. SQLException:操作数据库时,查询表可能发生异常
    2. IOException:操作文件时,发生的异常
    3. FileNotFoundException:当操作一个不存在的文件时,发生异常
    4. ClassNotFoundException:加载类,而该类不存在时,异常
    5. EOFException:操作文件,到文件未尾,发生异常
    6. lllegalArguementException:参数异常
异常处理

异常处理就是当异常发生时,对异常处理的方式。

  • 异常处理的方式(二选一):

    1. try-catch-finally

      程序员再代码中捕获发生的异常,自行处理。

      try {
          
          代码/可能有异常
              
      } catch (Exception e) {
          // 捕获异常
          // 1. 当异常发生时,后续代码不会再执行,直接进入catch
          // 2. 系统将异常封装成Exception对象e,传递给catch
          // 3. 得到异常对象后,程序员自己处理
          // 4. 注意:如果没有发生异常,catch代码块不执行
      } finally {
          // 1. 不管try代码块是否有异常发生,始终要执行finally代码块中的内容
          // 2. 所以,通常将释放资源的代码,放在finally
          // 3. 如果没有finally也是可以的
      }
      
      • 如果try代码块有可能有多个异常,可以使用多个catch分别捕获不同的异常,进行相应的处理。但是要求子类异常写在前面,父类异常写在后面。

        try {
            
            代码/可能有异常
                
        } catch (NullPointerException e) {
            
        } catch (ArithmeticException e) {
            
        } catch (Exception e) { // 父类异常
            
        } finally {
            
        }
        
      • 可以进行try-finally配合使用,这种用法相当于没有捕获异常,因此程序执行完finally之后会直接崩掉/退出,后面的代码不会再执行。

        应用场景:执行一段代码,不管是否发生异常,都必须执行某个业务逻辑。

        try {
            
        } finally {
            
        }
        
    2. throws

      将发生的异常抛出,交给调用者(方法)来处理,最顶级的处理者就是JVM。

      如果程序员没有显示的处理异常,默认采用throws

      image-20231031164825211

      • 基本介绍

        1. 如果一个方法(中的语句执行时)可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理
        2. 在方法声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类
        public class Throw {
            public void f() throws Exception {
                // 使用throws抛出异常,让调用f方法的调用者(方法)处理
                // throws后面的异常类型可以是方法中产生的异常类型,也可以是他的父类Exception
                // throws关键字后面的也可以是 异常列表,即可以抛出多个异常
                
                
                // 代码块
            }
        }
        
      • 子类重写父类的方法时,对抛出异常的规定:子类重写的方法所抛出的异常类型要么和父类抛出的异常一致,要么为父类抛出异常了剋行的子类型。

自定义异常

异常类型不在Throwable中,需要自己写一个异常类。

public class CustomException {
    public static void main(String[] args) {
        int age = 180;
        // 要求范围再18 - 120 之间,否则抛出一个异常
        if(!(age >= 18 && age <= 120)) {
            throw new AgeException("年龄需要在 18~120 之间");
        }
        System.out.println("你的年龄范围正确");
    }
}

/***
 * 自定义异常
 */
class AgeException extends RuntimeException {
    public AgeException(String message) {  // 构造器
        super(message);
    }
}

image-20231031174524034

  • 一般情况下,自定义异常是继承 RuntimeException,即把自定义异常做成 运行时异常。

集合

集合框架体系图(记住)
  1. 单列集合(单个单个的元素)

image-20231102120224036

  1. 双列集合(键值对)

image-20231102120615168

List接口
Iterator迭代器遍历
public class Iterator_ {
    @SuppressWarnings("all")
    public static void main(String[] args) {
        Collection col = new ArrayList<>();

        col.add(new Book("三国演义", "罗贯中", 10.1));
        col.add(new Book("小李飞刀", "古龙", 5.1));
        col.add(new Book("红楼梦", "曹雪芹", 8.1));

//        System.out.println(col);
        // 遍历col
        // 1. 首先得到col对应的迭代器
        Iterator iterator = col.iterator();
        // 2. 使用while循环遍历(itit回车快捷键)
        // 显示所有的快捷键:ctrl + j
        while (iterator.hasNext()) { // 判断下一个是否还有数据
            // 返回下一个元素
            Object obj = iterator.next();
            System.out.println("obj = " + obj);
        }
        // 3. 当退出while时,这时iterator迭代器只想最后的元素。
        // 4. 如果希望再次遍历,需要重置迭代器
        iterator = col.iterator();
        System.out.println();
        while (iterator.hasNext()) {
            Object obj =  iterator.next();
            System.out.println("obj = " + obj);
        }
    }
}

image-20231102212618914

增强for循环
  • 使用增强for循环遍历col
  • 增强for循环底层仍然是迭代器,可以理解为简化版的迭代器遍历
  • 快捷键:I + 回车
public class IteratorFor_ {
    @SuppressWarnings("all")
    public static void main(String[] args) {
        Collection col = new ArrayList<>();

        col.add(new Book("三国演义", "罗贯中", 10.1));
        col.add(new Book("小李飞刀", "古龙", 5.1));
        col.add(new Book("红楼梦", "曹雪芹", 8.1));

        for (Object book : col) {
            System.out.println("book = " + book);
        }
    }
}
ArrayList扩容(源码)
  1. 线程不安全,单线程可以使用,效率高。

  2. ArrayList中维护了一个Object类型的数据elementData

    transient Object[] elementData; transient表示瞬间的、短暂的,表示该属性不会被序列化。

  3. 当床创建ArrayList对象时,如果使用的时无参构造器(ArrayList list = new ArrayList();),则初始elementData容量为0,第1次添加,则扩容elementData为10,如需再次扩容,则扩容elementData为1.5倍,多余创建的赋值为null。

    底层源码如下:

    private Object[] grow(int minCapacity) {
            int oldCapacity = elementData.length;
            if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                int newCapacity = ArraysSupport.newLength(oldCapacity,
                        minCapacity - oldCapacity, /* minimum growth */
                        oldCapacity >> 1           /* preferred growth */);
                return elementData = Arrays.copyOf(elementData, newCapacity);
            } else {
                return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
            }
        }
    
  4. 如果使用的是指定大小的构造器(ArrayList list1 = new ArrayList(8);),则初始elementData容量为指定大小,如需扩容,则直接扩容elementData的1.5倍。

Vector
  1. Vector底层也是一个对象数组:protected Object[] elementData;
  2. Vector是线程同步的,即线程安全,Vector嘞的操作方法带有 synchronized
  3. 在开发中,需要线程同步安全时,考虑使用Vector,但是效率较低。

image-20231104202549640

LinkedList
  1. LinkedList底层实现的了双向链表和双端队列特点;
  2. 可以添加任意元素(元素可以重复),包括null
  3. 线程不安全,没有实现同步

image-20231104211453229

  • 如何选择ArrayListi和LinkedList:
    1. 如果我们改查的操作多,选择ArrayList
    2. 如果我们增删的操作多,选择LinkedList
    3. 一般来说,在程序中,80%-90%都是查询,因此大部分情况下会选择ArrayList
    4. 在一个项目中,根据业务灵活选择,也可能这样,一个模块使用的是ArrayList,另外一个模块是LinkedList.
Set接口

基本介绍:

  1. 无序(添加和取出的顺序不一致),但是顺序是固定的,没有索引
  2. 不允许重复元素,所以最多包含一个 null

Set接口的常用方法

  • 和List一样,Set接口也是 Collection 的子接口,常用方法和Collection一样。

Set接口的遍历方式:

  1. 迭代器
  2. 增强for循环
  3. 不能使用索引的方式来获取
public class SetMethod {
    @SuppressWarnings("all")
    public static void main(String[] args) {
        Set set = new HashSet();
        set.add("john");
        set.add("tom");
        set.add("john");
        set.add("petter");
        set.add(null);
        System.out.println("set = " + set);

        // 遍历
        // 方式一:迭代器
        System.out.println("========");
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object obj =  iterator.next();
            System.out.println("obj = " + obj);
        }

        // 方式二:增强for
        System.out.println("========");
        for (Object o : set) {
            System.out.println("o = " + o);
        }
    }
}
HashSet

基本说明:

  1. HashSet实现了Set接口;

  2. HashSet实际上是HashMap,HashMap底层是(数组+链表+红黑树),源码如下:

    public HashSet() {
            map = new HashMap<>();
        }
    
  3. HashSet不能添加相同的元素/数据?**经典面试题 **

    public class HashSet01 {
        @SuppressWarnings("all")
        public static void main(String[] args) {
            Set set = new HashSet();
            // HashSet不能添加相同的元素/数据?
            set.add("tom"); // 添加成功
            set.add("tom"); // 添加失败
    
            set.add(new Dog("Jili")); // 添加成功
            set.add(new Dog("Jili")); // 添加成功
            System.out.println("set = " + set);
    
            // 再加深一下,非常经典的面试题
            // 看源码,做分析
            set.add(new String("zhw")); // 添加成功
            set.add(new String("zhw")); // 添加失败
            System.out.println("set = " + set);
    
        }
    }
    

()HashSet底层实现:

  1. HashSet底层是HashMap
  2. 添加一个元素时,先得到hash值-会转成->索引值
  3. 找到存储数据表table,看这个索引位置是否已经存放的有元素
  4. 如果没有,直接加入
  5. 如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后
  6. 在Java8中,如果一条链表的元素个数到达TREEIFY THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY CAPACITY(默认64)就会进行树化(红黑树)
LinkedHashSet

基本说明

  1. LinkedHashSet是HashSet的子类;
  2. LinkedHashSet底层是一个LInkedHashMap,底层维护了一个 数组 + 双向链表;
  3. LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,是的元素看起来是以插入顺序保存的;
  4. LinkedHashSet不允许添加重复元素。
TreeSet
  1. 当我们使用无参构造器,创建TreeSet时,仍然是无序的
  2. 老师希望添加的元素,按照字符串大小来排序
  3. 使用TreeSet提供的一个构造器,可以传入一个比较器(匿名内部类)并指定排序规则
public class TreeSet_ {
    @SuppressWarnings("all")
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                // 进行字符串大小比较
//                return ((String) o1).compareTo((String) o2);
                // 按照字符串长度比较
                return ((String) o1).length() - ((String) o2).length();
            }
        });
        // 添加数据
        treeSet.add("jack");
        treeSet.add("tom");
        treeSet.add("abc"); // 如果按照字符串长度进行比较,则无法加入进去,因为与tom长度相同

        System.out.println("treeSet = " + treeSet);
    }
}
Map接口

基本说明:

  1. Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value(双列元素);
  2. Map中的key和value可以是任何引用类型的数据,会封装到HashMaps$Node对象中;
  3. Map中的key不允许重复,原因和HashSet一样;
  4. Map中的value可以重复;
  5. Map的key可以为nul,value也可以为null,注意key为null,只能有一个value为null,可以多个;
  6. 常用String类作为Map的key;
  7. key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value。
public class Map_ {
    @SuppressWarnings("all")
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("1", "1.1");
        map.put("2", "2.2");
        //添加重复元素
        map.put("1", "1.1.1"); // 当有相同的key,替换以前存在的key对应的value
        map.put("3", "2.2"); // value可以重复
        map.put(null, null); // 可以添加
        map.put(null, "abc"); // “abc”替换上一行的value——null
        map.put(new Object(), "123"); // key可以是任何类型

        System.out.println("map = " + map);
        // 通过get方法,传入key,返回对应的value
        System.out.println(map.get("1")); // 1.1.1
    }
}

image-20231105214303639

  1. Map存放数据的key-value示意图,一对k-v是放在一个HashMap$Node中的,又因为Node实现了Entry接口,有些书上也说一对k-v就是一个Entry(如图)

    image-20231105214659592

  2. Map接口遍历

    1. 先取出所有的Key,通过Key取出对应的Value

      Set keySet = map.KeySet();
      
      1. 增强for

        for (Object key : keySet) {
            System.out.println(key + "-" + map.get(key));
        }
        
      2. 迭代器

        Iterator iterator = keySet.iterator();
        while(iterator.hasNext()) {
            Object key = iterator.next();
            System.out.println(key + "-" + map.get(key));
        }
        
    2. 把所有的values取出

      Collection values = map.values();
      
      1. 增强for

        for (Object value : values) {
            System.out.println(value);
        }
        
      2. 迭代器

        Iterator iterator = values.iterator();
        while(iterator.hasNext()) {
            Object value = iterator.next();
            System.out.println(value);
        }
        
    3. 通过 EntrySet来获取 k-v

      Set entrySet = map.entrySet(); // EntrySet<Map.Entry<K, V>>
      
      1. 增强for

        for (Object entry : entrySet) {
            // 将entry转化为Map.Entry
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey() + "-" + m.getValue);
        }
        
      2. 迭代器

        Iterator iterator = entrySet.iterator();
        while(iterator.hasNext()) {
            Object entry = iterator.next();
            // 向下转型
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey() + "-" + m.getValue);
        }
        
Hashtable

基本介绍:

  1. 存放的元素是键值对:即K-V;
  2. hashtablel的键和值都不能为null,否则会抛出NullPointerException;
  3. hashTable使用方法基本上和HashMap一样;
  4. hashTable是线程安全的(synchronized),hashMap是线程不安全的。

底层机制:

  1. 底层有数组Hashtable$Entry[]初始化大小为11;
  2. 临界值thresho1d8=11*0.75;
  3. 扩容:按照自己的扩容机制来进行即可;
  4. 执行方法 addEntry(hash, key, value, index);添加K-V封装到Entry

image-20231107203012909

properties(也是Map接口的实现类,Maptable的子类)
  1. Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据。
  2. 他的使用特点和Hashtable类似
  3. Properties还可以用于从x.properties文件中,加载数据到Properties类对象,并进行读取和修改
  4. 说明:工作后x.properties文件通常作为配置文件,这个知识点在IO流举例;
TreeMap
public class TreeMap_ {
    @SuppressWarnings("all")
    public static void main(String[] args) {
        TreeMap treeMap = new TreeMap(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                // 进行K字符串大小比较
                return ((String) o1).compareTo((String) o2);
//                 按照K字符串长度比较
//                return ((String) o1).length() - ((String) o2).length();
            }
        });
        // 添加数据
        treeMap.put("jack", "杰克");
        treeMap.put("tom", "汤姆");
        treeMap.put("abc", "ABC"); // 如果按照字符串长度进行比较,则无法加入进去,因为与tom长度相同

        System.out.println("treeMap = " + treeMap);
    }
}
选择合适的集合类(记住)
  1. 先判断存储的类型(一组对象【单例】或一组键值对【双列】);
  2. 一组对象【单例】:Collection接口
    • 允许重复:LIst
      • 增删多:LinkedList【底层维护了有ige双向链表】
      • 改查多:ArrayList【底层维护Object类型的可变数组】
    • 不允许重复:Set
      • 无序:HashSet【底层是HashMap,维护了一个哈希表(即数组 + 链表 + 红黑树)】
      • 排序:TreeSet
      • 查如何取出顺序一致:LinkedHashSet【维护数组 + 双向链表】
  3. 一组键值对【双列】:Map
    • 键无序:HashMap【底层是:哈希表jdk7:数组+链表,jdk8:数组+链表+红黑树】
    • 键排序:TreeMap
    • 键插入和取出顺序一致:LinkedHashMap
    • 读取文件Properties
Collections工具类

基本介绍

  1. Collections是一个操作Set、List和Map等集合的工具类;
  2. Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作

排序操作

  1. reverse(List):反转List中元素的顺序;
  2. shuffle(List):对List集合元素进行随机排序;
  3. sot(List):根据元素的自然顺序对指定List集合元素按升序排序;
  4. sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序;
  5. swap(List,int,int):将指定list集合中的i处元素和j处元素进行交换。

查找、替换

  1. Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
  2. Object max(Collection,Comparator):根据Comparator指定的l顺序,返回给定集合中的最大元素
  3. Object min(Collection)
  4. Object min(Collection, Comparator)
  5. int frequency(Collection,Object):返回指定集合中指定元素的出现次数
  6. void copy(List dest,List src):将src中的内容复制到dest中
  7. boolean replaceAll(List list,Object oldVal,Object newVal):新值替换Lst对象的所有旧值

泛型

传统方法创建ArrayList数组

ArrayList dogs = new ArrayList();

泛型方法创建ArrayList数组

ArrayList<Dog> dogs = new ArrayList<Dog>();

// public class ArryList<E> {} E称为泛型,那么Dog -> E

一个对比示例:

public class Generic01 {
    @SuppressWarnings("all")
    public static void main(String[] args) {
        // 使用泛型向ArrayList中添加三个Dog对象
        // ArrayList<Dog>编译时会检查元素的类型,提高了安全性
            // 如果我加了一个Cat类,会编译报错
        // 遍历时减少了类型转换的次数,提高效率
            // 对于传统的方法,for (Object o : dogs){},然后在循环体内需要向下转型为Dog
            // 使用泛型for (Dog dog : dogs){}不需要向下转型

        // 传统
        ArrayList dogs = new ArrayList();
        // 泛型
        ArrayList<Dog> dogs = new ArrayList<Dog>();
        dogs.add(new Dog("旺财", 3));
        dogs.add(new Dog("大黄", 2));
        dogs.add(new Dog("小黑", 1));

        // 遍历
        // 传统
        for (Object o : dogs) {
            Dog dog = (Dog) o;
            System.out.println(dog.getName() + dog.getAge());
        }
        // 泛型
        for (Dog dog : dogs) {
            System.out.println(dog.getName() + dog.getAge());
        }

    }
}

泛型的好处:

  1. 编译时,检查添加元素的类型,提高了安全性
  2. 减少了类型转换的次数,提高效率
  3. 不再提示编译警告
泛型介绍
  • 泛(广泛)型(类型):表示数据类型(Integer,String,Dog,)的一种数据类型。

  • 泛型又称参数化类型,是Jdk5.0出现的新特性,解决数据类型的安全性问题

  • 在类声明或实例化时只要指定好需要的具体的类型即可。

  • Jva泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。同时,代码更加简洁、健壮

  • 泛型的作用是:可以在类声明时通过一个标识表示类中某个属性的类型,或者是某个方法的返回值的类型,或者是参数类型。[有点难]

    class Person<E> {
        E s; // E表示s的数据类型,该数据类型在定义Person对象的时候指定,即在编译期间,就确定E是什么类型。
        
        public Person(E s) { // E也可以是参数类型
            this.s = s;
        }
        
        public E f() { // 返回类型使用E
            return s;
        }
    }
    

    在使用时:

    Person<String> person = new Person<String>("Honvin");
    
    // 相当于Person类如下定义:
    class Person {
        String s; 
        
        public Person(String s) {
            this.s = s;
        }
        
        public String f() {
            return s;
        }
    }
    

    注意:E具体的数据类型在定义Person对象的时候指定,即在编译期间,就确定E是什么类型。

泛型的语法
  • 泛型的声明:

    // 接口泛型
    interface 接口<T> {}
    
    // 类泛型
    class<K,V> {}
    
    /**
    说明:
    1. 其中,T,K,V不代表值,而是表示类型。
    2. 人以字母都可以,常用T表示,(Type的缩写)
    **/
    
  • 泛型的实例化:

    要在类名后面指定类型参数的值(类型)。如:

    List<String> strList = new ArrayList<String>();
    Iterator<Customer>iterator customers.iterator();
    
  • 一个实例:

    分别使用HashSet和HashMap添加三个Student对象,并遍历

    @SuppressWarnings("all")
    public class GenericExercise {
        public static void main(String[] args) {
            // 1. HashSet
            System.out.println("1. HashSet");
            Set<Student> set = new HashSet<Student>();
            set.add(new Student("tom", 22));
            set.add(new Student("john", 12));
            set.add(new Student("petter", 18));
    //        set.add(18); // 报错
            // 迭代器遍历
            System.out.println("======迭代器遍历======");
            Iterator<Student> iterator = set.iterator();
            while (iterator.hasNext()) {
                Student student = iterator.next();
                System.out.println(student.name + ":" + student.age);
            }
            // 增强for遍历
            System.out.println("======增强for遍历======");
            for (Student student : set) {
                System.out.println(student.name + ":" + student.age);
            }
    
            System.out.println();
    
            // 2. HashMap
            System.out.println("2. HashMap");
            Map<String, Student> map = new HashMap<String, Student>();
            map.put("jerry", new Student("jerry", 20));
            map.put("maston", new Student("maston", 25));
            map.put("dave", new Student("dave", 30));
    
            Set<Map.Entry<String, Student>> entrySet = map.entrySet();
            // 迭代器遍历
            System.out.println("======迭代器遍历======");
            Iterator<Map.Entry<String, Student>> iterator1 = entrySet.iterator();
            while (iterator1.hasNext()) {
                Map.Entry<String, Student> entry = iterator1.next();
                System.out.println(entry.getKey() + ":" + entry.getValue().age);
            }
            // 增强for遍历
            System.out.println("======增强for遍历======");
            for (Map.Entry<String, Student> entry : entrySet) {
                System.out.println(entry.getKey() + ":" + entry.getValue().age);
            }
    
    
        }
    }
    
    @SuppressWarnings("all")
    class Student {
        public String name;
        public int age;
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    }
    
泛型说明
  1. 给泛型的指定类型要求必须是引用类型Integer,String等,不能是基本数据类型int,char等;

  2. 在给泛型指定具体类型之后,可以传入该类型或者其子类型;

  3. 泛型的使用形式:

    ArrayList<String> strList = new ArrayList<String>();
    List<String> strList = new ArrayList<String>();
    

    在实际开发中,往往简写:

    ArrayList<String> strList = new ArrayList<>();
    List<String> strList = new ArrayList<>();
    
    // 编译器会进行类型推断
    
  4. 如果这样写,泛型默认是Object。

    List list = new ArrayList();
    // 等价于下面的写法
    List<Object> list = new ArrayList<>();
    
自定义泛型
自定义泛型类
  • 基本语法

    class 类名<T, R...> { // ...表示可以有多个泛型
        成员
    }
    

    注意细节:

    1. 普通成员可以使用泛型(属性、方法)

    2. 使用泛型的数组,不能初始化

      class Tiger<T, R, M> {
          T[] ts;  // 正确
          T[] tsf = new T[8]; // 错误
      }
      
      • 因为数组在 new 时不能确定T的类型,就无法在内存开空间。
    3. 静态方法中不能使用类的泛型

      class Tiger<T, R, M> {
          R r;  // 正确
          static R r1;  // 错误
          public static void m(M m) {}  // 错误
      }
      
      • 因为静态是和类相关的,在类加载时,对象还没有创建,所以,如果静态方法和静态属性使用了泛型,JVM就无法完成初始化。
    4. 泛型类的类型,是在创建对象时确定的(因为创建对象时,需要指定确定类型)

    5. 如果在创建对象时,没有指定类型,默认为Object

自定义泛型接口
  • 基本语法

    interface 接口名<T, R...> {
        
    }
    

    注意细节:

    1. 接口中,静态成员(接口的成员默认都是静态的)也不能使用泛型(这个和泛型类规定一样)

      interface 接口名<T, R...> {
          T name; // 错误(默认静态)
      }
      
    2. 泛型接口的类型,在继承接口或者实现接口时确定

      interface A<U, R>{}
      // 接口AA继承接口A
      interface AA extends A<String, Double>{}
      // 类AAA实现接口A
      class AAA implements A<String, Double>{}
      // 下面两个写法等价,但是推荐第二种
      class B implements A{}
      class B implements A<Object, Object>{}
      
    3. 没有指定类型,默认为Object

自定义泛型方法
  • 基本语法

    修饰符 <T, R...> 返回类型 方法名(参数列表) {}
    

    注意细节:

    1. 泛型方法,可以定义在普通类中,也可以定义在泛型类中

      public <T, R> void f(T t, R r){}
      
      • <T, R>就是泛型,是提供给fly使用的
    2. 当泛型方法被调用时,类型会确定,且会自动装箱(int -> Integer)

    3. public void eat(Ee){},修饰符后没有<T,R.> eat 方法不是泛型方法而是使用了泛型

    4. 泛型方法可以使用类声明的泛型,也可以使用自己声明的泛型

      class A<T> {
          public <R> void f(T t, R r) {}
      }
      
泛型的继承和通配符
  1. 泛型不具备继承性

    Object o = new String("xx");  // String继承了Object,是可以的
    List<Object> list = new ArrayList<String>();  // 但是泛型是不具备继承的,所以是错误的
    
  2. <?>:支持任意泛型类型

  3. <? extends A>:支持A类以及A类的子类,规定了泛型的上限

  4. <? super A>:支持A类以及A类的父类,不限于直接父类,规定了泛型的下限

线程(基础)

线程相关概念
  • 程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。
    简单的说:就是我们写的代码

  • 进程:运行的程序。启动一个进程,操作系统就会为该进程分配内存空间。

    【程序】 —运行—>【进程】

    进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程。

  • 线程:

    1. 线程由进程创建的,是进程的一个实体;
    2. 一个进程可以拥有多个线程。
  • 单线程:同一个时刻,只允许执行一个线程。

  • 多线程,同一个时刻,可以执行多个线程。

  • 并发:同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉。简单的说,单核CPU实现的多任务就是并发。

  • 并行:同一个时刻,多个任务同时执行。多核CPU可以实现并行。

    (并发和并行可以同时存在)

线程的基本使用

创建线程的两种方式

  1. 继承Thread类,重写run方法
  2. 实现Runnable接口,重写run方法【建议】

示例1:继承Thread类,重写run方法

public class Thread01 {
    public static void main(String[] args) {
        // 创建一个线程对象
        Cat cat = new Cat();
        // 启动线程->执行cat的run方法
        cat.start();
        /***
        	注意:这里不能用cat.run();
        		cat.run();只是一个普通的方法,不会开启一个线程执行,而是会在main线程中执				   行,这样使用不会得到如下交替输出的结果。而是等cat.run();执行结束之后才会				执行下面的for循环
        **/
        
        // 说明:当main线程启动一个子线程Thread-0时。主线程不会阻塞,会继续执行
        System.out.println("主线程继续执行:" + Thread.currentThread().getName()); // 名字 main
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程 i=" + i);
            // 让主线程休眠
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

// 当一个类继承了Thread类,该类就可以当作线程使用
// 重写run方法,写上自己的业务逻辑
// run Thread 类实现了Runnable接口的run方法
class Cat extends Thread {
    @Override
    public void run() { // 重写run方法,写上自己的业务逻辑
        int times = 0;
        while (true) {
            // 每隔一秒,控制台输出“喵喵,我是小猫咪”
            System.out.println("喵喵,我是小猫咪" + (++times) + "\t线程名:" + Thread.currentThread().getName());

            // 让线程休眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if (times == 5) break; // 5次之后退出
        }
    }
}

image-20231112193114590

注意:

  1. 若main线程和其他线程一块执行,当main线程结束之后,但还有其他线程执行的时候,此时并不意味着进程执行结束,只有所有的线程结束之后才意味着进程的结束。
  2. 这里main方法中不能用cat.run();cat.run();只是一个普通的方法,不会开启一个线程执行,而是会在main线程中执行,这样使用不会得到如下交替输出的结果。而是等cat.run();执行结束之后才会执行下面的for循环。

问题:当Cat类已经继承其他类时,就不能再继承Thread类。

示例2:实现Runnable接口,重写run方法

public class Thread02 {

    public static void main(String[] args) {
        // 创建一个Dog对象
        Dog dog = new Dog();
        // 创建一个Thread对象,并将dog对象(实现Runnable)作为参数传入,放入到Thread
        // 用到了代理模式的静态代理
        Thread thread = new Thread(dog);
        // 启动线程
        thread.start();
    }
}

class Dog implements Runnable {
    private int count = 0;

    @Override
    public void run() {
        while (true) {
            System.out.println("汪汪汪" + (++count) + "\t线程名:" + Thread.currentThread().getName());

            // 休眠1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            if (count == 10) break;
        }
    }
}

image-20231112200247372

示例3:多线程实现:

public class Thread03 {
    public static void main(String[] args) {
        T1 t1 = new T1();
        T2 t2 = new T2();
        Thread thread1 = new Thread(t1);
        Thread thread2 = new Thread(t2);
        thread1.start();
        thread2.start();
        // 下面也会创建一个新的线程与上述的同时执行
        T1 t11 = new T1();
        Thread thread11 = new Thread(t11);
        thread11.start();
    }
}

class T1 implements Runnable {
    private int cnt = 0;

    @Override
    public void run() {
        while (true) {
            // 每个1s输出“hello word”,输出10次
            System.out.println("hello word" + "\t" + (++cnt) +  "\t" + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            if (cnt == 10) break;
        }
    }
}

class T2 implements Runnable {
    private int cnt = 0;

    @Override
    public void run() {
        while (true) {
            // 每个1s输出“hi”,输出5次
            System.out.println("hi" + "\t" + (++cnt) +  "\t" + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            if (cnt == 5) break;
        }
    }
}

image-20231112201320877

线程终止

基本说明:

  1. 当线程完成任务后,会自动退出。

  2. 还可以通过使用变量来控制run方法退出的方式停止线程,即通知方式。

    public class ThreadExit {
        public static void main(String[] args) throws InterruptedException {
            T t = new T();
            Thread thread = new Thread(t);
            thread.start();
    
            System.out.println("main线程休眠5s");
            Thread.sleep(5 * 1000);
            t.setLoop(false); // 通知线程退出
        }
    }
    
    class T implements Runnable {
        private static int cnt = 0;
        private boolean loop = true; // 设置一变量控制线程是否退出
    
        public void setLoop(boolean loop) {
            this.loop = loop;
        }
    
        @Override
        public void run() {
            while (loop) {
                // 休眠50毫秒
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
    
                System.out.println("T is running...." + (++cnt));
            }
        }
    }
    
线程常用方法
  1. setName 设置线程名称,使之与参数name相同

  2. getName 返回该线程的名称

  3. start 使该线程开始执行;Java虚拟机底层调用该线程的start0方法

  4. run 调用线程对象run方法;

  5. setPriority 更改线程的优先级

  6. getPriority 获取线程的优先级

  7. sleep 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)

  8. interrupt 中断线程,但没有真正的结束线程。所以一般用于中断正在休眠的线程

    可以使处于休眠中的线程提前结束。

这些方法在main方法或者其他方法中调用时使用。

  1. yield:线程的礼让。让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功

  2. join:线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务,然后再执行被插入的线程。

    public class ThreadMethod {
        public static void main(String[] args) throws InterruptedException {
            T t = new T();
            t.start();
            for (int i = 1; i <= 20; i++) {
                Thread.sleep(1000);
                System.out.println("主线程让子线程先吃完之后,主线程再吃");
                if (i == 5) t.join(); // 线程插队,让线程t先完成任务,main线程再继续任务
                System.out.println("主线程吃了" + i + "个包子");
            }
        }
    }
    
    class T extends Thread {
        @Override
        public void run() {
            for (int i = 1; i <= 20; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("子线程吃了" + i + "个包子");
            }
        }
    }
    
用户线程和守护线程
  • 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束

  • 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束

    public class ThreadDaemon {
        public static void main(String[] args) throws InterruptedException {
            MyDaemonThread myDaemonThread = new MyDaemonThread();
            // 设置守护线程
            myDaemonThread.setDaemon(true);
            // 然后启动守护线程
            myDaemonThread.start();
            for (int i = 0; i < 3; i++) {
                System.out.println("main run");
                Thread.sleep(1000);
            }
        }
    }
    
    class MyDaemonThread extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("MyDaemonThread run");
            }
        }
    }
    

    image-20231113111108489

    主线程(用户线程)结束,守护线程不会继续执行,也会跟着结束

    • 常见的守护线程:垃圾回收机制
线程的生命周期(重要)

Java官方API将线程的整个生命周期分为六个状态,分别是NEW(新建状态)、RUNNABLE(可运行状态)、BLOCKED(阻塞状态)、WAITING(等待状态)、TIMED_WAITING(定时等待状态)和TERMINATED(终止状态)。

1615964642254_线程生命周期.png

参考:Java多线程:线程的生命周期的六种状态

线程同步机制

理解:

  1. 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。
  2. 也可以这里理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。

实现同步-Synchronized

  1. 同步代码块

    synchronized(对象) { // 得到对象的锁,才能操作同步代码
        // 需要被同步代码
    }
    
  2. synchronized还可以放在方法生命中,表示整个方法为同步方法

    public synchronized void m(String name) {
    	// 需要呗同步的代码
    }
    
互斥锁
  1. Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。可以使用synchronized实现;

  2. 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象;

  3. 关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问;

  4. 同步的局限性:导致程序的执行效率要降低;

  5. 同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象);

    class Test implements Runnable{
        Object obj = new Object();
        public void m() {
        	synchronized(this/*或者 obj*/) {
                
            }
    	}
    }
    
  6. 同步方法(静态的)的锁为当前类本身。

    class Test implements Runnable{
        public static void m() {
        	synchronized(Test.class) {
                
            }
    	}
    }
    

注意:

  1. 同步方法如果没有使用static修饰:默认锁对象为this
  2. 如果方法使用static修饰,默认锁对象:当前类.class
  3. 实现的落地步骤:
    • 需要先分析上锁的代码
    • 选择同步代码块或同步方法
    • 要求多个线程的锁对象为同一个即可!
线程死锁

基本介绍:

  • 多个线程都占用了对方的锁资源,但不肯相让,导致了死锁,在编程是一定要避免死锁的发生。
释放锁

下面的操作会释放锁:

  • 当前线程的同步方法、同步代码块执行结束
    • 案例:上厕所,完事出来
  • 当前线程在同步代码块、同步方法中遇到break、return。
    • 案例:没有正常的完事,经理叫他修改bug,不得已出来
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
    • 案例:没有正常的完事,发现忘带纸,不得已出来
  • 当前线程在同步代码块、同步方法中执行了线程对象的wt0方法,当前线程暂停,并释放锁。
    • 案例:没有正常完事,觉得需要酝酿下,所以出来等会再进去

下面的操作不会释放锁:

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,不会释放锁
    • 案例:上厕所,太困了,在坑位上眯了一会
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起该线程不会释放锁。
    • 提示:应尽量避免使用suspend()和resume()来控制线程,方法不再推荐使用

IO流

文件

保存数据的地方

  • 文件流

    • 文件在程序中以流的形式来操作。

      输出流
      输入流
      java程序(内存)
      文件(磁盘)
  • 常用的文件操作

    • 创建文件对象相关构造器和方法

      new File(String pathname)  // 根据路径构建一个File对象
      new File(File parent, String child)  // 根据父目录文件 + 子路径构建
      new File(String parent, String child)  // 根据父目录 + 子路径构建
      

      示例:

      public class FileCreate {
          public static void main(String[] args) {
      
          }
          // 方式1 new File(String pathname)
          @Test
          public void createFile1() {
              String filePath = "E:\\workspace\\java\\javaBase\\output\\file\\news1.txt";
              File file = new File(filePath);  // 在内存中创建 file 对象
      
              try {
                  file.createNewFile();  // 将 file 对象写入到磁盘中,使之真正成为一个文件
                  System.out.println("文件创建成功。");
              } catch (IOException e) {
                  throw new RuntimeException(e);
              }
          }
      
          // 方式2 new File(File parent, String child)
          // E:\workspace\java\javaBase\output\file\news2.txt
          @Test
          public void createFile2() {
              File parentFile = new File("E:\\workspace\\java\\javaBase\\output\\file\\");
              String fileName = "news2.txt";
              File file = new File(parentFile, fileName);
      
              try {
                  file.createNewFile();
                  System.out.println("文件创建成功。");
              } catch (IOException e) {
                  throw new RuntimeException(e);
              }
          }
      
          //方式3 new File(String parent, String child)
          // E:\workspace\java\javaBase\output\file\news3.txt
          @Test
          public void createFile3() {
              String parentPath = "E:\\workspace\\java\\javaBase\\output\\file\\";
              String fileName = "news3.txt";
              File file = new File(parentPath, fileName);
      
              try {
                  file.createNewFile();
                  System.out.println("文件创建成功。");
              } catch (IOException e) {
                  throw new RuntimeException(e);
              }
          }
      }
      
    • 获取文件信息

      • getName、getAbsolutePath、getParent、length(文件大小-字节)、exists(文件是否存在)、isFile(是不是一个文件)、isDirectory(是不是一个目录)

      示例:

      public class FileInformation {
          // 获取文件信息
          @Test
          public void info() {
              // 先创建文件对象
              String filePath = "E:\\workspace\\java\\javaBase\\output\\file\\news1.txt";
              File file = new File(filePath);  // 在内存中创建 file 对象
      
              // 调用相应的方法,得到对应信息
              System.out.println("文件名:" + file.getName());
          }
      }
      
    • 目录的操作和文件删除

      • mkdir:创建一级目录

      • mkdirs:创建多级目录

      • delete:删除空目录或文件

        示例

        public class Directory_ {
            // 判断文件E:\workspace\java\javaBase\output\file\news1.txt是否存在,如果存在就删除
            @Test
            public void m1() {
                // 先创建文件对象
                String filePath = "E:\\workspace\\java\\javaBase\\output\\file\\news1.txt";
                File file = new File(filePath);  // 在内存中创建 file 对象
        
                if (file.exists()) {
                    if (file.delete()) {
                        System.out.println(filePath + " 删除成功 ");
                    } else {
                        System.out.println(filePath + " 删除失败 ");
                    }
                } else
                    System.out.println(filePath + " 不存在");
            }
        
            // 判断目录E:\workspace\java\javaBase\output\file\demo 是否存在,不存在就创建
            @Test
            public void m2() {
                String dirPath = "E:\\workspace\\java\\javaBase\\output\\file\\demo";
                File dir = new File(dirPath);
                if (!dir.exists()) {
                    if (dir.mkdirs()) {
                        System.out.println(dirPath + " 创建成功 ");
                    } else {
                        System.out.println(dirPath + " 创建失败 ");
                    }
                } else
                    System.out.println(dirPath + " 目录已存在");
            }
        }
        
IO流原理

java IO流原理

  • I/O是Input/Output的缩写,I/O技术用于处理数据传输,如读/写文件,网络通讯等。
  • Java程序中,对于数据的输入/输出操作以“流(stream)”的方式进行。

流的分类:

  • 按操作数据单位不同分为:字节流(8bt)二进制文件,字符流(按字符)文本文件

  • 按数据流的流向不同分为:输入流,输出流

  • 按流的角色的不同分为:节点流,处理流/包装流

    抽象基类字节流字符流
    输入流InputStreamReader
    输出流OutputStreamWriter
FileInputStream

示例:

/**
 * @ClassName: FileInputStream_
 * @Date: 2023/11/21 18:24
 * @Author: Honvin
 * @Software: IntelliJ IDEA
 * @Description: 演示字节FileInputStream的使用(字节输入流  文件 --> 程序)
 **/
public class FileInputStream_ {
    public static void main(String[] args) {

    }
    /**
     * 演示读取效果
     * 单个字节的读取,效率比较低
     */
    @Test
    public void readFile01() {
        String filePath = "E:\\workspace\\java\\javaBase\\data\\file\\hello.txt";
        int readData = 0;
        FileInputStream fileInputStream = null;
        try {
            // 创建 FileInputStream 对象,用于读取文件
            fileInputStream = new FileInputStream(filePath);
            // 从该输入流读取一个字节的数据,如果没有输入可用,此方法将停止
            // 如果返回 -1 ,表示读取完毕
            while ((readData = fileInputStream.read()) != -1) {
                System.out.print((char) readData);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                assert fileInputStream != null;
                // 关闭文件流,释放资源
                fileInputStream.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 演示读取效果
     * 使用read(byte[] b)读取文件,提高效率
     */
    @Test
    public void readFile02() {
        String filePath = "E:\\workspace\\java\\javaBase\\data\\file\\hello.txt";
        // 定义字节数组
        byte[] buf = new byte[8]; // 一次读取8个字节
        int readLen = 0;
        FileInputStream fileInputStream = null;
        try {
            // 创建 FileInputStream 对象,用于读取文件
            fileInputStream = new FileInputStream(filePath);
            // 从该输入流读取一个字节的数据,如果没有输入可用,此方法将停止
            // 如果返回 -1 ,表示读取完毕
            // 如果读取正常,返回实际读取的字节数
            while ((readLen = fileInputStream.read(buf)) != -1) {
                System.out.print(new String(buf, 0, readLen));  // 显示
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                assert fileInputStream != null;
                // 关闭文件流,释放资源
                fileInputStream.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
FileOutputStream

示例:

public class FileOutputStream_ {
    /**
     * 演示使用 FileOutputStream 将数据写道文件中
     * 如果该文件不存在,则创建文件
     */
    @Test
    public void writeFile() {
        // 创建FileOutputStream对象
        String filePath = "E:\\workspace\\java\\javaBase\\output\\hello.txt";
        FileOutputStream fileOutputStream = null;

        try {
            // 得到FileOutputStream对象
            /**
             * 追加和覆盖
             * new FileOutputStream(filePath); 当写入内容时,会覆盖原来的内容
             * new FileOutputStream(filePath, true); 写入内容时,会追加到文件的后面(第二个参数是append)
             */
            fileOutputStream = new FileOutputStream(filePath);
            // 写入一个字节
            fileOutputStream.write('H');
            // 写入字符串
            String str = "Hello world!";
            // str.getBytes()可以得到该字符串的字节数组
            fileOutputStream.write(str.getBytes());

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                assert fileOutputStream != null;
                fileOutputStream.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
FileRead

示例;

public class FileReader_ {
    /**
     * 单个字符读取
     */
    @Test
    public void readFile01() {
        String filePath = "E:\\workspace\\java\\javaBase\\data\\file\\hello.txt";
        FileReader fileReader = null;
        int data = 0;
        try {
            // 创建 FileReader对象
            fileReader = new FileReader(filePath);
            // 循环读取,使用read,单个字符读取
            while ((data = fileReader.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                assert fileReader != null;
                fileReader.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 字符数组读取
     */
    @Test
    public void readFile02() {
        String filePath = "E:\\workspace\\java\\javaBase\\data\\file\\hello.txt";
        FileReader fileReader = null;
        int readLen = 0;
        char[] buf = new char[8];
        try {
            // 创建 FileReader对象
            fileReader = new FileReader(filePath);
            // 循环读取,使用read(buf),返回实际读取到的字符数
            while ((readLen = fileReader.read(buf)) != -1) {
                System.out.print(new String(buf, 0, readLen));
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                assert fileReader != null;
                fileReader.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
FileWrite

示例:

public class FileWriter_ {
    @Test
    public void writeFile() {
        String filePath = "E:\\workspace\\java\\javaBase\\output\\hello.txt";
        FileWriter fileWriter = null;
        try {
            fileWriter = new FileWriter(filePath);
            fileWriter.write("H");
            // 数据量较大时,使用循环
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                // 不关闭无法写入
                assert fileWriter != null;
                fileWriter.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
节点流和处理流
  • 节点流可以从一个特定的数据源续写数据,如FileReader、FileWriter

    image-20231123103112796

    数据源:就是存放数据的地方

  • 处理流(也叫包装流)是“连接”已存在的流(节点流或处理流)之上,微程序提供更为强大的读写功能,也更加灵活,如BufferedReader、BufferedWriter

    image-20231123104004241

  • 常见的节点流和处理流

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 节点流和处理流的区别和联系

    • 节点流是底层流/低级流,直接跟数据源相接
    • 处理流(包装流)包装节点流,既可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入输出。
    • 处理流(也叫包装流)对节点流进行包装,使用了修饰器设计模式,不会直接与数据源相连[模拟修饰器设计模式]
  • 处理流的功能主要体现在以下两个方面:

    • 性能的提高:主要以增加缓冲的方式来提高输入输出的效率。
    • 操作的便捷:处理流可能提供了一系列便捷的方法来一次输入输出大批量的数据,使用更加灵活方便。
BufferedReader

示例:

public class BufferedReader_ {
    public static void main(String[] args) throws IOException {
        String filePath = "E:\\workspace\\java\\javaBase\\data\\file\\hello.txt";
        // 创建BufferedReader对象
        BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath));
        // 读取
        String line;  // 按行读取
        // 下面代码按行读取
        // 当返回null时,表示文件读取完毕
        while ((line = bufferedReader.readLine()) != null) {
            System.out.println(line);
        }

        // 关闭流,只需要关闭 BufferedReader,FileReader 内部会自动关闭
        bufferedReader.close();
    }
}
BufferedWriter

示例:

public class BufferedWriter_ {
    public static void main(String[] args) throws IOException {
        String filePath = "E:\\workspace\\java\\javaBase\\output\\hello.txt";
        // 创建BufferedWriter对象
        BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filePath));  // 覆盖
//        BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filePath, true));  // 追加
        bufferedWriter.write("hello honvin");
        bufferedWriter.newLine();  // 添加换行
        bufferedWriter.write("hello world");
        bufferedWriter.newLine();

        // 关闭流
        bufferedWriter.close();

    }
}
Buffered字节处理流(BufferedInputStream & BufferedOutputStream)

BufferedInputStream

BufferedOutputStream

二进制文件拷贝示例:

public class BufferedCopy {
    public static void main(String[] args) {
        String inPath = "E:\\workspace\\java\\javaBase\\data\\file\\test.jpg";
        String outPath = "E:\\workspace\\java\\javaBase\\output\\file\\test.jpg";

        // 创建 BufferedInputStream 和 BufferedOutputStream 对象
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;

        try {
            bis = new BufferedInputStream(new FileInputStream(inPath));
            bos = new BufferedOutputStream(new FileOutputStream(outPath));

            // 循环读取文件,并写入到outPath
            byte[] buff = new byte[1024];
            int readLen = 0;
            // 当返回 -1 时,表示文件读取完毕
            while ((readLen = bis.read(buff)) != -1) {
                bos.write(buff, 0, readLen);
            }
            System.out.println("文件拷贝完毕");
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            // 关闭流
            try {
                if (bos != null) {
                    bos.close();
                }
                if (bis != null) {
                    bis.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
对象流-ObjectInputStream & ObjectOutputStream

需求:

  1. 将int num=100这个int数据保存到文件中,注意不是100数字,而是int100,并且,能够从文件中直接恢复int 100
  2. 将Dog dog=new Dog(“"小黄”,3)这个dog对象保存到文件中,并且能够从文件恢复.
  3. 上面的要求,就是能够将基本数据类型或者对象进行序列化反序列化操作

序列化和反序列化

  1. 序列化就是在保存数据时,保存数据的值和数据类型

  2. 反序列化就是在恢复数据时,恢复数据的值和数据类型

  3. 需要让某个对象支持序列化机制,则必须让其类是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一:

    • Serializable (推荐,这是一个标记接口)

    • Externalizable (该接口有方法需要实现)

应用案例:

使用ObjectOutputStream序列化基本数据类型和一个Dog对象(name,age),并保存到data.dat文件中。

public class ObjectOutputStream_ {
    public static void main(String[] args) throws Exception {
        // 序列化后,保存的文件格式,不是存文本,而是按照他自己的格式来保存
        String filePath = "E:\\workspace\\java\\javaBase\\output\\file\\data.dat";

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));

        // 序列化数据到 E:\workspace\java\javaBase\output\file\data.dat
        oos.write(100);  // int --> Integer(Integer 实现了Serializable接口)
        oos.writeBoolean(true);  // bool --> Boolean
        oos.writeChar('a');  // char --> Character
        oos.writeDouble(9.5);  // double --> Double
        oos.writeUTF("honvin");  // String实现了Serializable接口

        // 保存一个Dog对象(注意:Dog对象必须实现Serializable接口)
        oos.writeObject(new Dog("旺财", 10));

        oos.close();
        System.out.println("数据保存完毕(序列化形式)");
    }
}

// Dog对象必须实现Serializable接口
class Dog implements Serializable {
    private String name;
    private int age;

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

使用ObjectInputStrea读取data.dat文件并反序列化恢复数据。

package com.honvin.base.file.inputstream;

import com.honvin.base.file.outputstream.Dog;

import java.io.FileInputStream;
import java.io.ObjectInputStream;

/**
 * @ClassName: ObjectInputStream_
 * @Date: 2023/11/23 15:38
 * @Author: Honvin
 * @Software: IntelliJ IDEA
 * @Description:
 **/
public class ObjectInputStream_ {
    public static void main(String[] args) throws Exception {
        // 指定反序列化的文件
        String filePath = "E:\\workspace\\java\\javaBase\\output\\file\\data.dat";

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));

        // 读取
        // 读取(反序列化)的顺序需要和保存数据(序列化)的顺序一致,否则会出现异常
        System.out.println(ois.readInt());
        System.out.println(ois.readBoolean());
        System.out.println(ois.readChar());
        System.out.println(ois.readDouble());
        System.out.println(ois.readUTF());
        Object dog = ois.readObject();
        System.out.println("运行类型 = " + dog.getClass());
        System.out.println("dog信息 = " + dog);  // 底层:Object --> Dog

        // 注意细节:
        // 1. 如果我们需要调用Dog的方法,需要向下转型
        // 2. 需要将Dog类引入
        Dog dog2 = (Dog)dog;
        System.out.println(dog2.getName());  // 旺财

        // 关闭流(关闭外层流即可)
        ois.close();
    }
}

注意事项和细节

  1. 读写顺序要一致;
  2. 要求实现序列化或反序列化对像,需要实现Serializable接口;
  3. 序列化的类中建议添加SerialVersionUlD(序列话的版本号,可以提高兼容性),为了提高版本的兼容性;
  4. 序列化对象时,默认将里面所有属性都进行序列化,但除了statici或**transient(短暂的,表示不序列化)**修饰的成员;
  5. 序列化对象时,要求里面属性的类型也需要实现序列化接口(该类中包含其他类时,其他类也需要实现序列化接口);
  6. 序列化具备可继承性,也就是如果某类已经实现了序列化,则它的所有子类也已经默认实现了序列化。
标准输入输出流
编译类型运行类型默认设备
System.in 标准输入InputStreamBufferedInputStream键盘
System.out 标准输出PrintStreamPrintStream显示器
Scanner scanner = new Scanner(System.in);
String next = scanner.next();  // 输入

System.out.println("next = " + next);  // 输出
转换流-InputStreamReader & OutputStreamWriter
  1. InputStreamReader::Reader的子类,可以将InputStream(字节流)包装(转换)成Reader(字符流)
  2. OutputStreamWriter::Writer的子类,实现将OutputStream(字节流)包装成Writer(字符流)
  3. 当处理纯文本数据时,如果使用字符流效率更高,并且可以有效解决中文问题,所以建议将字节流转换成字符流
  4. 可以在使用时指定编码格式(比如utf-8,gbk,gb2312,IS08859-1等)

InputStreamReader 应用案例:

InputStreamReader 转换流解决中文乱码问题

将字节流 FileInputStream 转换成字符流 InputStreamReader,指定编码 gbk/utf-8

/**
 * @ClassName: InputStreamReader_
 * @Date: 2023/11/23 16:19
 * @Author: Honvin
 * @Software: IntelliJ IDEA
 * @Description: 演示 InputStreamReader 转换流解决中文乱码问题
 *               将字节流 FileInputStream 转换成字符流 InputStreamReader,指定编码 gbk/utf-8
 **/
public class InputStreamReader_ {
    public static void main(String[] args) throws Exception {
        String filePath  = "E:\\workspace\\java\\javaBase\\data\\file\\a.txt";  // 该文件为GBK编码

        // 将字节流 FileInputStream 转换成字符流 InputStreamReader,指定编码 gbk
//        InputStreamReader isr = new InputStreamReader(new FileInputStream(filePath), "gbk");
        // 将 InputStreamReader 传入 BufferedReader
//        BufferedReader br = new BufferedReader(isr);

        // 把上述两句合在一起
        BufferedReader br = new BufferedReader(new InputStreamReader(
                                                    new FileInputStream(filePath), "gbk"));

        // 读取
        String s = br.readLine();
        System.out.println("读取的内容 = " + s);
        // 关闭流
        br.close();
    }
}

OutputStreamWriter 应用案例:

演示 OutputStreamWriter 使用

把 FileOutputStream 字节流转换成字符流 OutputStreamWriter,指定编码 gbk/utf8

/**
 * @ClassName: OutputStreamWriter_
 * @Date: 2023/11/23 16:29
 * @Author: Honvin
 * @Software: IntelliJ IDEA
 * @Description: 演示 OutputStreamWriter 使用
 *               把 FileOutputStream 字节流转换成字符流 OutputStreamWriter,指定编码 gbk/utf8
 **/
public class OutputStreamWriter_ {
    public static void main(String[] args) throws IOException {
        String filePath = "E:\\workspace\\java\\javaBase\\output\\file\\b.txt";
        String charSet = "gbk";
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(filePath), charSet);
        osw.write("你好");
        osw.close();
        System.out.println("按照 " + charSet + " 编码写入完毕");
    }
}
打印流-PrintStream(字节流) & PrintWriter(字符流)

打印流只有输出流,没有输入流

PrintStream 应用案例:

演示PrintStream (字节打印流/输出流)

public class PrintStream_ {
    public static void main(String[] args) throws IOException {
        PrintStream out = System.out;
        // 在默认情况下,PrintStream 输出数据的位置是 标准输出 即 显示器
        out.print("Hi,Beijing");
        /*
            public void print(String s) {
                write(String.valueOf(s));
            }
         */
        // 由于 print 的底层是 write 也可以这样写
        out.write("Hi, BJUT".getBytes());

        out.close();

        // 可以修改打印流输出的设备
        // 修改输出到E:\workspace\java\javaBase\output\file\printstream.txt
        // "Hi, Beijing" 就会写入到文件中而不会在显示器显示
        System.setOut(new PrintStream("E:\\workspace\\java\\javaBase\\output\\file\\printstream.txt"));
        System.out.println("Hi, Beijing");
    }
}

PrintWrite 应用案例:

public class PrintWriter_ {
    public static void main(String[] args) throws IOException {
        // 输出到显示器
        PrintWriter pw = new PrintWriter(System.out);
        pw.print("Hi, Beijing");

        pw.close();

        // 输出到文件
        PrintWriter pw1 = new PrintWriter(new FileWriter("E:\\workspace\\java\\javaBase\\output\\file\\printwriter.txt"));
        pw1.print("Hi, Beijing");

        pw1.close();
    }
}
Properties类

基本介绍:

  1. 专门用于读写配置文件的集合类;
    配置文件的格式:
    键=值
    键=值
  2. 注意:键值对不需要有空格,值不需要用引号一起来。默认类型是String。
  3. Properties的常见方法
    • Ioad:加载配置文件的键值对到Properties对象
    • list:将数据显示到指定设备
    • getProperty(key):根据键获取值
    • setProperty(key,value):设置键值对到Properties对象
    • store:将Propertiesr中的键值对存储到配置文件,在idea中,保存信息到配置文件,如果含有中文,会存储为unicodet码。

Properties应用案例:

  1. 使用Properties类完成对mysql.properties的读取
public class Properties_ {
    public static void main(String[] args) throws IOException {
        // 使用 Properties 类 来读取 mysql.properties 文件
        // 1. 创建 Properties 类的对象
        Properties  properties = new Properties();
        // 2. 调用 load 方法加载文件
        properties.load(new FileInputStream("src\\mysql.properties"));
        // 3. 把 k-v 显示出来
        properties.list(System.out);
        // 4. 根据 key 获取特定的值
        String user = properties.getProperty("user");
        String pwd = properties.getProperty("pwd");
        // 5. 显示 user 和 pwd
        System.out.println("user = " + user);
        System.out.println("pwd = " + pwd);
    }
}
  1. 使用Properties类完成对mysql2.properties的读取并修改某个 k-v
public class Properties02 {
    public static void main(String[] args) throws IOException {
        // 使用 Properties 类来创建配置文件,修改配置文件内容
        Properties properties = new Properties();
        // 创建
        // 如果key不存在,则创建
        // 如果key存在,则修改
        properties.setProperty("charset", "utf8");
        properties.setProperty("user", "汤姆");
        properties.setProperty("pwd", "abc");

        // 将 k-v 存储文件中
        properties.store(new FileWriter("src\\mysql01.properties"), null);
        System.out.println("保存配置文件成功");
    }
}

网络编程

相关概念
  • 网络通信

    1. 概念:两台设备之间通过网络实现数据传输

    2. 网络通信:将数据通过网络从一台设备传输到另一台设备

    3. java.net包下提供了一系列的类或接口,供程序员使用,完成网络通信

  • 网络

    1. 概念:两台或多台设备通过一定物理设备连接起来构成了网络
    2. 根据网络的覆盖范围不同,对网络进行分类:
      • 局域网:覆盖范围最小,仅仅覆盖一个教室或一个机房
      • 城域网:覆盖范围较大,可以覆盖一个城市
      • 广域网:覆盖范围最大,可以覆盖全国,甚至全球,万维网是广域网的代表
    主机1
    网络
    主机2
    主机3
    网络设备
  • ip地址

    1. 概念:用于唯一标识网络中的每台计算机/主机

    2. 查看ip地址:ipconfig

    3. ip地址的表示形式:点分十进制 XX.XX.XX.XX(127.0.0.1是自己的本机地址)

    4. 每一个十进制数的范围:0~255

      0~2550~2550~2550~255
    5. ip地址的组成=网络地址(192.168.16)+主机地址(69),比如:192.168.16.69

    6. IPv6是互联网工程任务组设计的用于替代IPv4的下一代IP协议,其地址数量号称可以为全世界的每一粒沙子编上一个地址;

    7. 由于IPv4最大的问题在于网络地址资源有限,严重制约了互联网的应用和发展。IPv6的使用,不仅能解决网络地址资源数量的问题,而且也解决了多种接入设备连入互联的障碍;

    8. IPv4的地址长度为4字节32位;IPv6的地址长度为16字节128位

    9. IPv4地址分类:

      image-20231125182705061

      image-20231125192718305

  • 域名

    • www.baidu.com
    • 好处:为了方便记忆,解决记ip的困难;
    • 概念:将ip地址映射成域名(HTTP协议)
  • 端口号

    1. 概念:用于标识计算机上某个特定的网络程序
    2. 表示形式:以整数形式,范围0~65535【两个字节表示端口 2^16-1】
    3. 01024已经被占用,比如ssh22,ftp21,smtp25http80【在网络开发中不要使用01024】
    4. 常见的网络程序端口号:
      • tomcat:8080
      • mysql:3306
      • oracle:1521
      • sqlserver:1433
  • 网络协议

    • 在网络编程中,数据的组织形式就是协议

    • 协议(TCP/IP):TCP/IP(Transmission ControlProtocol/lnternet Protocol的简写中文译名为传输控制协议/因特网互联协议,又叫网络通讯协议,这个协议是Internet最基本的协议、Internet国际互联网络的基础,简单地说,就是由网络层的IP协议和传输层的TCP协议组成的。

      image-20231125194817677

    • OSI模型是一种理论模型,计算机中并没有,实际中使用TCP/IP模型。

      image-20231125194938901

      • 应用层 – 应用程序
      • 传输层 – TCP
      • 网络层 – IP
  • TCP 和 UDP

    • TCP协议:传输控制协议

      1. 使用TCP协议前,须先建立TCP连接,形成传输数据通道

      2. 传输前,采用“三次握手“方式,是可靠的

        图片

      3. TCP协议进行通信的两个应用进程:客户端、服务端

      4. 在连接中可进行大数据量的传输5.传输完毕,需释放已建立的连接,效率低

      5. 两者建立连接之后,第三者进不来

    • UDP协议:用户数据协议

      1. 将数据、源、目的封装成数据包,不需要建立连接
      2. 每个数据报的大小限制在64K内(比较小,不适合传输大量数据)
      3. 因无需连接,故是不可靠的
      4. 发送数据结束时无需释放资源(因为不是面向连接的),速度快
      5. 举例:厕所通知:发短信
InetAddress类
  1. 获取本机InetAddress对象getLocalHost
  2. 根据指定主机名/域名获取ip地址对象getByName
  3. 获取lnetAddress对象的主机名getHostName
  4. 获取lnetAddress对象的地址getHostAddress
public class API_ {

    public static void main(String[] args) throws Exception {
        // 获取本机的 InetAddress 对象
        InetAddress localHost = InetAddress.getLocalHost();
        System.out.println(localHost);  // DESKTOP-ELDL7HA/192.168.253.1

        // 根据指定的竹居明 获取 InetAddress 对象
        InetAddress host1 = InetAddress.getByName("DESKTOP-ELDL7HA");
        System.out.println(host1);  // DESKTOP-ELDL7HA/192.168.243.1

        // 根据域名返回 InetAddress 对象,比如 www.baidu.com
        InetAddress host2 = InetAddress.getByName("www.baidu.com");
        System.out.println(host2);  // www.baidu.com/182.61.200.7

        // 通过 InetAddress 对象,获取对应的地址
        String hostAddress = host2.getHostAddress();
        System.out.println(hostAddress);  //182.61.200.7

        // 通过 InetAddress 对象,获取主机名/域名
        String hostName = host2.getHostName();
        System.out.println(hostName);  // www.baidu.com
    }
}
电脑
1.获取本机的信息[主机名/IP]
2.通过域名获取远程服务器的IP
服务器
Socket
  1. 套接字(Socket)开发网络应用程序被广泛采用,以至于成为事实上的标准。
  2. 通信的两端都要有Socket,是两台机器间通信的端点。
  3. 网络通信其实就是Socket间的通信。
  4. Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。
  5. 一般主动发起通信的应用程序属客户端,等待通信请求的为服务端。

image-20231125201720861

TCP网络通信编程,可靠
  1. 基于客户端一服务端的网络通信
  2. 底层使用的是TCP/IP协议
  3. 应用场景举例:客户端发送数据,服务端接受并显示
  4. 基于Socket的TCP编程
  5. 当客户端连接到服务端后,实际上客户端也是通过一个端口和服务端进行通讯的,这个端口是TCP/IP来分配的,是不确定的,是随机的。

image-20231125202035464

应用案例1:使用字节流

  1. 编写一个服务器端,和一个客户端
  2. 服务器端在9999端口监听
  3. 客户端连接到服务端,发送“hello,server”,并接收服务器端回发的"hello,client",再退出
  4. 服务器端接收到客户端发送的信息,输出,并发送“hello,client",再退出
  • 注意:写入之后需要设置写入结束标记socket.shutdownOutput();
package com.honvin.base.socket;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @ClassName: SocketTCP01Server
 * @Date: 2023/11/25 20:26
 * @Author: Honvin
 * @Software: IntelliJ IDEA
 * @Description: 服务端
 **/
public class SocketTCP01Server {
    public static void main(String[] args) throws IOException {
        // 思路LI
        // 1。在本机的9999端口监听,等待连接
        // 注意:要求在本机没有其他服务在监听 9999 端口
        // 这个 serverSocket 可以通过 accept() 返回多个 Socket[多个客户端连接服务器(多并发)]
        ServerSocket serverSocket = new ServerSocket(9999);
        System.out.println("服务端,在 9999 端口接听,等待链接。");
        // 2。当没有客户端连接9999端口时,程序会阻塞,等待连接
        // 如果有客户端链接,则会返回 Socket 对象,程序继续执行
        Socket socket = serverSocket.accept();
        System.out.println("服务端 socket = " + socket.getClass());
        // 3。通过socket。getInputStream() 读取U客户端写入到数据通道的数据,显示
        InputStream inputStream = socket.getInputStream();
        // 4. IO读取
        byte[] buf = new byte[1024];
        int readLine = 0;
        while ((readLine = inputStream.read(buf)) != -1) {
            System.out.println(new String(buf, 0, readLine));  // 根据读取到的实际长度,显示内容
        }
        // 5. 获取 socket 相关联的输出流
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("hello,client".getBytes());
        // 设置结束标记
        socket.shutdownOutput();
        // 6. 关闭流和socket
        outputStream.close();
        inputStream.close();
        socket.close();
        serverSocket.close();
        System.out.println("服务端,关闭流和socket。");
    }
}

package com.honvin.base.socket;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;

/**
 * @ClassName: SocketTCP01Client
 * @Date: 2023/11/25 20:32
 * @Author: Honvin
 * @Software: IntelliJ IDEA
 * @Description: 客户端,发送“Hello server”给服务段
 **/
public class SocketTCP01Client {
    public static void main(String[] args) throws IOException {
        //思路
        // 1。连接服务端(ip,端口)
        // 链接本机的 9999 端口,如果链接成功,返回 Socket 对象
        Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
        System.out.println("客户端 socket = " + socket.getClass());
        // 2。连接上后,生成Socket,通过socket.getOutputStream() 得到和 socket 对象关联的输出流对象
        OutputStream outputStream = socket.getOutputStream();
        // 3。通过输出流,写入数据到数据通道
        outputStream.write("Hello server".getBytes());  // "Hello server".getBytes() 得到"Hello server"的字节数组
        // 设置结束标记
        socket.shutdownOutput();
        // 4. 获取和socket相关联的输入流,读取数据(字节),并显示
        InputStream inputStream = socket.getInputStream();
        byte[] buf =  new byte[1024];
        int readLine = 0;
        while ((readLine = inputStream.read(buf)) != -1) {
            System.out.println(new String(buf, 0, readLine));
        }
        // 5. 关闭流对象和socket
        inputStream.close();
        outputStream.close();
        socket.close();
        System.out.println("客户端关闭");
    }

}

应用案例2:使用字符流

  1. 编写一个服务器端,和一个客户端
  2. 服务器端在9999端口监听
  3. 客户端连接到服务端,发送“hello,server”,并接收服务器端回发的"hello,client",再退出
  4. 服务器端接收到客户端发送的信息,输出,并发送“hello,client",再退出
  • 注意:可以使用bw.newLine(); 插入一个换行符,表示写入内容结束,要求对方使用readLine()来读
package com.honvin.base.socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @ClassName: SocketTCP01Server
 * @Date: 2023/11/25 20:26
 * @Author: Honvin
 * @Software: IntelliJ IDEA
 * @Description: 服务端
 **/
public class SocketTCP02Server {
    public static void main(String[] args) throws IOException {
        // 思路LI
        // 1。在本机的9999端口监听,等待连接
        // 注意:要求在本机没有其他服务在监听 9999 端口
        // 这个 serverSocket 可以通过 accept() 返回多个 Socket[多个客户端连接服务器(多并发)]
        ServerSocket serverSocket = new ServerSocket(9999);
        System.out.println("服务端,在 9999 端口接听,等待链接。");
        // 2。当没有客户端连接9999端口时,程序会阻塞,等待连接
        // 如果有客户端链接,则会返回 Socket 对象,程序继续执行
        Socket socket = serverSocket.accept();
        System.out.println("服务端 socket = " + socket.getClass());
        // 3。通过socket。getInputStream() 读取U客户端写入到数据通道的数据,显示
        InputStream inputStream = socket.getInputStream();
        // 4. IO读取,使用字符流, 使用 InputStreamReader 将 inputStream 转成字符流
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        String s = br.readLine();
        System.out.println(s);  // 输出

        // 5. 获取 socket 相关联的输出流
        OutputStream outputStream = socket.getOutputStream();
        // 使用字符输出流的方式回复信息
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outputStream));
        bw.write("hello, client 字符流");
        bw.newLine();
        bw.flush();
        // 6. 关闭流和socket
        bw.close();
        br.close();
        socket.close();
        serverSocket.close();
        System.out.println("服务端,关闭流和socket。");
    }
}
package com.honvin.base.socket;

import java.io.*;
import java.net.InetAddress;
import java.net.Socket;

/**
 * @ClassName: SocketTCP01Client
 * @Date: 2023/11/25 20:32
 * @Author: Honvin
 * @Software: IntelliJ IDEA
 * @Description: 客户端,发送“Hello server”给服务段
 **/
public class SocketTCP02Client {
    public static void main(String[] args) throws IOException {
        //思路
        // 1。连接服务端(ip,端口)
        // 链接本机的 9999 端口,如果链接成功,返回 Socket 对象
        Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
        System.out.println("客户端 socket = " + socket.getClass());
        // 2。连接上后,生成Socket,通过socket.getOutputStream() 得到和 socket 对象关联的输出流对象
        OutputStream outputStream = socket.getOutputStream();
        // 3。通过输出流,写入数据到数据通道,使用字符流
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outputStream));
        bw.write("hello, server 字符流");
        bw.newLine();  // 插入一个换行符,表示写入内容结束,要求对方使用readLine()来读
        bw.flush();  // 如果使用的字符流,需要手动刷新,否则数据不会写入数据通道
        // 4. 获取和socket相关联的输入流,读取数据(字节),并显示
        InputStream inputStream = socket.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        String s = br.readLine();
        System.out.println(s);
        // 5. 关闭流对象和socket
        br.close();
        bw.close();
        socket.close();
        System.out.println("客户端关闭");
    }
}

应用案例3:网络上传文件

  1. 编写一个服务端,和一个客户端
  2. 服务器端在8888端口监听
  3. 客户端连接到服务端,发送一张图片e:\qie.png
  4. 服务器端接收到客户端发送的图片,保存到src下,发送“收到图片”再退出
  5. 客户端接收到服务端发送的“收到图片”,再退出
  6. 该程序要求使用StreamUtils.java
package com.honvin.base.upload;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @ClassName: TCPFIleUploadServer
 * @Date: 2023/11/25 21:26
 * @Author: Honvin
 * @Software: IntelliJ IDEA
 * @Description: 文件上传服务端
 **/
public class TCPFIleUploadServer {
    public static void main(String[] args) throws Exception {
        // 服务端在本机监听8888端口
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("服务端在8888端口监听。。。。");
        //  等待客户端连接
        Socket socket = serverSocket.accept();

        // 读取客户端发送的数据
        // 同归哦Socket得到输入流
        BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
        byte[] bytes = StreamUtils.streamToByteArray(bis);
        // 将得到的bytes写入到指定的路径,就得到一个文件了
        String descFilePath = "output/test.jpg";
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(descFilePath));
        bos.write(bytes);

        // 向客户端恢复“收到图片”
        // 通过socket获取输出流(字符)
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        bw.write("收到图片");
        bw.newLine();
        bw.flush();

        // 关闭资源
        bw.close();
        bos.close();
        bis.close();
        socket.close();
        serverSocket.close();
    }
}

package com.honvin.base.upload;

import java.io.*;
import java.net.InetAddress;
import java.net.Socket;

/**
 * @ClassName: TCPFIleUploadClient
 * @Date: 2023/11/25 21:26
 * @Author: Honvin
 * @Software: IntelliJ IDEA
 * @Description: 文件上传客户端
 **/
public class TCPFIleUploadClient {
    public static void main(String[] args) throws Exception {
        // 创建客户端连接服务端,得到Socket对象
        Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
        // 创建读取磁盘文件的输入流
        String filePath = "source/img/test.jpg";
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath));

        // bytes 就是 filePath 对应的字节数组
        byte[] bytes = StreamUtils.streamToByteArray(bis);

        // 通过 socket 获取到输出流,将bytes数据发送给服务段
        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
        bos.write(bytes);  // 将文件对应的字节数组的内容,写入到数据通道
        socket.shutdownOutput();  // 设置写入数据的结束标记

        // 通过 socket 获取到输入流,读取服务端的回复
        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String s = br.readLine();
        System.out.println(s);

        // 关闭相关的流
        br.close();
        bos.close();
        bis.close();
        socket.close();
    }
}
netstat
  1. netstat -an可以查看当前主机网络情况,包括端口监听情况网络连接情况
  2. netstat -an | more可以分页显示
  3. 要求在dos控制台下执行

image-20231126204543194

UDP编程,不可靠
基本介绍
  1. 类DatagramSocket和DatagramPacket[数据包/数据报]实现了基于UDP协议网络程序。
  2. UDP数据报通过数据报套接字DatagramSocket发送和接收,系统不保证UDP数据报一定能够安全送到目的地,也不能确定什么时候可以抵达。
  3. DatagramPacket对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号。
  4. UDP协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接。

image-20231126211052820

UDP说明:

  1. 没有明确的服务端和客户端,演变成数据的发送端和接收端;
  2. 接收数据和发送数据是通过 DatagramSocket 对象完成;
  3. 发送数据会将数据封装到 DatagramPacket 对象中(装包);
  4. 当接收到 DatagramPacket 对象之后们需要进行拆包,取出数据;
  5. DatagramSocket 可以指定在哪个端口接收数据。
基本流程
  1. 核心的两个类/对象DatagramSocket与DatagramPacket
  2. 建立发送端,接收端(没有服务端和客户端概念)
  3. 发送数据前,建立数据包/报DatagramPacket对象
  4. 调用DatagramSocket的发送、接收方法
  5. 关闭DatagramSocket。

应用案例:

  1. 编写一个接收端A,和一个发送端B
  2. 接收端A在9999端口等待接收数据(receive)
  3. 发送端B向接收端A发送数据“hello,明天吃火锅~
  4. 接收端A接收到发送端B发送的数据,回复“好的,明天见”,再退出
  5. 发送端接收回复的数据,再退出
package com.honvin.base.udp;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * @ClassName: UDPReceiverA
 * @Date: 2023/11/26 21:13
 * @Author: Honvin
 * @Software: IntelliJ IDEA
 * @Description: UDP接收端A
 **/
public class UDPReceiverA {
    public static void main(String[] args) throws IOException {
        // 创建一个 DatagramSocket 对象,准备在9999端口接收数据
        DatagramSocket socket = new DatagramSocket(9999);
        // 构建一个 DatagramPacket 对象,准备接收数据
        // 一个 UDP 数据包最大64K
        byte[] buf = new byte[64 * 1024];
        DatagramPacket packet = new DatagramPacket(buf, buf.length);
        // 调用 接受方法,将通过网络传输的 DatagramPacket 对象填充到 packet 对象
        // 当有数据包发送到本机的9999端口时,就会接受到数据
        // 如果没有数据包发送到本机的9999端口,就会一直阻塞
        System.out.println("接收端A等待接收数据");
        socket.receive(packet);
        // 可以把 packet 进行拆包,取出数据,并显示
        int length = packet.getLength();  // 实际接收到的数据字节长度
        byte[] data = packet.getData();  // 接受到数据
        String s = new String(data, 0, length);
        System.out.println(s);
        System.out.println("A 端接收数据成功");

        // ====回复B 好的,明天见
        byte[] sendData = "好的,明天见".getBytes();
        DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, InetAddress.getByName("192.168.253.1"), 9998);
        socket.send(sendPacket);
        System.out.println("A 端发送数据成功");

        // 关闭资源
        socket.close();
        System.out.println("A 端退出");
    }
}
package com.honvin.base.udp;

import java.io.IOException;
import java.net.*;

/**
 * @ClassName: UDPSenderB
 * @Date: 2023/11/26 21:13
 * @Author: Honvin
 * @Software: IntelliJ IDEA
 * @Description: UDP发送端B(也可以接收数据)
 **/
public class UDPSenderB {
    public static void main(String[] args) throws IOException {
        // 创建一个 DatagramSocket 对象,准备在9998端口接收数据
        DatagramSocket socket = new DatagramSocket(9998);
        // 将需要放的数据封装到 DatagramPacket 对象中
        byte[] data = "hello,明天吃火锅~".getBytes();
        // 封装的 DatagramPacket 对象:data 内容字节数组, data.length 内容长度, InetAddress 地址,端口
        DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("192.168.253.1"), 9999);
        socket.send(packet);
        System.out.println("B 端发送数据成功");

        // ====等待接受A回复
        byte[] bytes = new byte[64 * 1024];
        DatagramPacket receivePacket = new DatagramPacket(bytes, bytes.length);
        System.out.println("B端等待接收数据");
        socket.receive(receivePacket);
        int length = receivePacket.getLength();
        String s = new String(bytes, 0, length);
        System.out.println("B端收到数据:" + s);
        System.out.println("B 端接收数据成功");

        // 关闭资源
        socket.close();
        System.out.println("B 端退出");
    }
}

java8 新特性

Lambda表达式
  • lambda表达式是一种函数式编程

  • 语法:

    (形参列表) -> lambda体;
    
    // 例如:
    (s1, s2) -> {
        return s1.compareTo(s2);
    };
    
    // 当只有一条语句时,可以把{}去掉,且去掉return
    (s1, s2) -> s1.compareTo(s2);
    

    说明:

    ->:lambda操作符或箭头操作符

    ->的左边:lambda形参列表,对应着重写接口中的抽象方法的形参列表

    ->的右边:lamba体,队以ing这接口的实现类要重写的方法体

    示例:

    public class Lambda_ {
        public static void main(String[] args) {
    
            Runnable r1 = new Runnable() {  // 匿名函数
                @Override
                public void run() {
                    System.out.println("r1 Hello World");
                }
            };
            r1.run();
            System.out.println("------------------------------------");
    
    		// lambda表达式写法
            Runnable r2 = () -> {
                System.out.println("r2 Hello World");
            };
            r2.run();
            System.out.println("------------------------------------");
    
            // 当只有一条语句时,可以把{}去掉
            Runnable r3 = () -> System.out.println("r3 Hello World");
            r3.run();
        }
    }
    
  • lambda表达式的本质

    1. lambda表达式作为接口的实现类的对象
    2. lambda表达式是一个匿名函数
函数接口
  • 什么是函数接口?

    若接口中只有一个抽象方法,则此接口就成为函数式接口。

  • 为什么使用函数接口?

    只有给函数式接口提供实现类的对象时,才可以使用lambda表达式。

  • 4个基本的函数式接口

接口对应的抽象方法
消费性接口Consumervoid accept(T t)
供给型接口SupplierT get()
函数型接口Function<T, R>R apply(T t)
判断型接口Predicateboolean test(T t)
方法引用

前提:选哟熟悉函数式接口及其抽象方法的使用

  • 理解:

    • 可以看成是lambda表达式的进一步刻画
    • 函数式接口满足一定条件下可以使用方法引用或构造器引用替换lambda表达式
  • 本质:方法引用作为了函数式接口的实例

  • 格式:

    类(或者对象)::方法名

  • 具体格式:

    1. 对象::实例方法

      public void Test1() {
          Consumer<String> con1 = new Consumer<String>() {
              @Override
              public void accept(String s) {
                  System.out.println(s);
              }
          };
          con1.accept("Hello con1");
      
          // lambda表达式
          Consumer<String> con2 = s -> System.out.println(s);
          con2.accept("Hello con2");
      
          // 方法引用
          Consumer<String> con3 = System.out::println;
          con2.accept("Hello con3");
      }
      
    2. 类::静态方法

      public void Test2() {
          Function<Double, Long> func1 = new Function<Double, Long>() {
              @Override
              public Long apply(Double aDouble) {
                  return Math.round(aDouble);
              }
          };
          System.out.println(func1.apply(1.111));
      
          // lambda表达式
          Function<Double, Long> func2 = aDouble -> Math.round(aDouble);
          System.out.println(func1.apply(2.111));
      
          // 方法引用
          Function<Double, Long> func3 = Math::round;
          System.out.println(func1.apply(3.111));
      
      }
      
    3. 类::实例方法

      public void Test3() {
          Comparator<String> com1 = new Comparator<String>() {
              @Override
              public int compare(String s1, String s2) {
                  return s1.compareTo(s2);
              }
          };
          System.out.println(com1.compare("abc", "abb"));
      
          // lambda表达式
          Comparator<String> com2 = (s1, s2) -> s1.compareTo(s2);
          System.out.println(com2.compare("abc", "abb"));
      
          // 方法引用
          Comparator<String> com3 = String::compareTo;
          System.out.println(com3.compare("abc", "abb"));
      }
      
      • 说明:第3种,形参列表一共有n个参数,其中第一个参数为方法的调用者 才可以用
构造器引用
  • 类似于方法引用

  • 格式:类名::new

  • 说明:

    调用了类名对应的类中的某一个构造器,具体调用的构造器取决于函数式接口的抽象方法的形参列表

    Supplier方法使用

    public void Test4() {
        Supplier<Employee> sup1 = new Supplier<Employee>() {
            @Override
            public Employee get() {
                return new Employee();
            }
        };
        System.out.println(sup1.get());
    
        // lambda表达式
        Supplier<Employee> sup2 = () -> new Employee();
        System.out.println(sup2.get());
    
        // 构造器引用
        Supplier<Employee> sup3 = Employee::new;
        System.out.println(sup3.get());
    }
    

Function方法使用

public void Test5() {
    Function<String, Employee> fun1 = new Function<String, Employee>() {
        @Override
        public Employee apply(String id) {
            return new Employee(id);
        }
    };
    System.out.println(fun1.apply("1"));

    // lambda表达式
    Function<String, Employee> fun2 = (id) -> new Employee();
    System.out.println(fun2.apply("2"));

    // 构造器引用
    Function<String, Employee> fun3 = Employee::new;
    System.out.println(fun3.apply("3"));

    BiFunction<String, String, Employee> fun4 = Employee::new;
    System.out.println(fun4.apply("3", "Tom"));
}
数组引用
  • 格式:数组名[]::new
Stream API
基本介绍

StreamAPI(java.util.stream)把真正的函数式编程风格引入到Java中。这是目前为止对Java类库最好的补充因为StreamAPi可以极大提供Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。

Stream是Java8中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用StreamAPI对集合数据进行操作,就类似于使用SQL执行的数据库查询。也可以使用StreamAPI来并行执行操作。简言之,StreamAPI提供了一种高效且易于使用的处理数据的方式。

为什么要使用 Stream API

实际开发中,项目中多数数据源都来自于MySQL、Oracle等。但现在数据源可以更多了,有MongDB,Radis等,而这些NoSQL的数据就需要Java层面去处理。

Stream API vs 集合框架
  • StreamAPI关注的是多个数据的计算(排序、查找、过滤、映射、遍历等),面向CPU的。
  • 集合关注的数据的存储,面向内存的。
  • StreamAPI之于集合,类似于SQL之于数据表的查询。
使用说明

①stream自己不会存储元素。

②Stream不会改变源对象。相反,他们会返回一个持有结果的新Stream。

③Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。即一旦执行终止操作,就执行中间操作链,并产生结果。

④Stream一旦执行了终止操作,就不能再调用其它中间操作或终止操作了。

Stream API执行流程
  1. 步骤1:Stream的实例化

    public class StreamAPITest {
    
        // 创建 Stream 方式一:通过集合
        @Test
        public void test1() {
            List<Employee> list = EmployeeData.getEmployees();
            // 返回一个顺序流
            Stream<Employee> stream = list.stream();
            // 返回一个并行流
            Stream<Employee> employeeStream = list.parallelStream();
    
            System.out.println(stream);
            System.out.println(employeeStream);
        }
    
        // 创建 Stream 方式二:通过数组
        @Test
        public void test2() {
            Integer[] arr = new Integer[]{1, 2, 3, 4, 5};
            Stream<Integer> stream = Arrays.stream(arr);
    
            int[] arrInt = new int[]{1, 2, 3, 4};
            IntStream intStream = Arrays.stream(arrInt);
        }
    
        // 创建 Stream 方式三:通过Stream的of()
        @Test
        public void test3() {
            Stream<String> stream = Stream.of("a", "b", "c");
        }
    }
    
  2. 步骤2:一系列的中间操作

    1. 筛选与切片

      	@Test
          public void test1() {
      //        filter(Predicate p)——接收 Lambda,从流中排除某些元素
              // 查询员工列表中薪资大于7000的员工的信息
              List<Employee> list = EmployeeData.getEmployees();
              Stream<Employee> stream = list.stream();
              stream.filter(employee -> employee.getSalary() > 7000).forEach(System.out::println);
              /*
                  Employee{id=1001, name='马化腾', age=34, salary=10000}
                  Employee{id=1003, name='刘强东', age=45, salary=20000}
               */
      
              System.out.println();
      //        limit(n)——截断流,使其元素不超过给定数量
              // 因为stream已经执行了终止操作,就不可以再调用其他操作,需要重新创建Stream;
              list.stream().limit(2).forEach(System.out::println);
              /*
                  Employee{id=1001, name='马化腾', age=34, salary=10000}
                  Employee{id=1002, name='马云', age=40, salary=5000}
               */
      
              System.out.println();
      //        skip(n)——跳过元素,返回一个扔掉了前 n 个元素的流,若流中元素不足 n 个,则返回一个空流。
              list.stream().skip(2).forEach(System.out::println);
              /*
                  Employee{id=1003, name='刘强东', age=45, salary=20000}
               */
      
              System.out.println();
      //        distinct()——筛选,通过流所生成元素的hashCode()和equals()取出重复元素
              list.add(new Employee(1004, "马斯克", 40, 12500));
              list.add(new Employee(1004, "马斯克", 40, 12500));
              list.add(new Employee(1004, "马斯克", 40, 12500));
              list.add(new Employee(1004, "马斯克", 40, 12500));
              list.stream().distinct().forEach(System.out::println);
              /*
                  Employee{id=1001, name='马化腾', age=34, salary=10000}
                  Employee{id=1002, name='马云', age=40, salary=5000}
                  Employee{id=1003, name='刘强东', age=45, salary=20000}
                  Employee{id=1004, name='马斯克', age=40, salary=12500}
               */
          }
      
    2. 映射

      // 2. 映射
          @Test
          public void test2() {
      //        map(Function f)——接收一个函数作为参数,将元素转换成其他形式或提取信息,该函数会被应用到每个元素上,并将其映
              // 练习:转换为大写
              List<String> list = Arrays.asList("aaa", "bbb", "ccc", "ddd");
              list.stream().map(str -> str.toUpperCase()).forEach(System.out::println);
              list.stream().map(String::toUpperCase).forEach(System.out::println);
              /*
                  AAA
                  BBB
                  CCC
                  DDD
               */
      
              // 练习:获取员工姓名长度大于3的员工。
              List<Employee> employees = EmployeeData.getEmployees();
              employees.stream().filter(employee -> employee.getName().length() >= 3).forEach(System.out::println);
              /*
                  Employee{id=1001, name='马化腾', age=34, salary=10000}
                  Employee{id=1003, name='刘强东', age=45, salary=20000}
               */
      
              // 练习:获取员工姓名长度大于3的员工的名字。
              employees.stream().filter(employee -> employee.getName().length() >= 3).map(Employee::getName).forEach(System.out::println);
              /*
                  马化腾
                  刘强东
               */
          }
      
    3. 排序

          // 3. 排序
          @Test
          public void test3() {
      //        sorted()——自然排序
              Integer[] arr = new Integer[]{345, 4, 54, 543, 2, 543, 34};
              String[] arr1 = new String[]{"DD", "AA", "KK", "BB"};
      
              Arrays.stream(arr).sorted().forEach(System.out::println);
              /*
                  2
                  4
                  34
                  54
                  345
                  543
                  543
               */
      
              Arrays.stream(arr1).sorted().forEach(System.out::println);
      
              // 因为Employee没有实现Comparable接口,所以报错
      //        List<Employee> employees = EmployeeData.getEmployees();
      //        employees.stream().sorted().forEach(System.out::println);
      
      //        sorted(Comparator<? super T> com)——定制排序
              List<Employee> employees = EmployeeData.getEmployees();
              employees.stream().sorted((e1, e2) -> e1.getAge() - e2.getAge()).forEach(System.out::println);
              /*
                  Employee{id=1002, name='马云', age=20, salary=5000}
                  Employee{id=1001, name='马化腾', age=34, salary=10000}
                  Employee{id=1003, name='刘强东', age=45, salary=20000}
               */
      
              // 针对字符串从大到小排列
              Arrays.stream(arr1).sorted((s1, s2) -> -s1.compareTo(s2)).forEach(System.out::println);
              /*
                  KK
                  DD
                  BB
                  AA
               */
          }
      
  3. 步骤3:执行终止操作

    1. 匹配与查找

          // 1. 匹配与查找
          @Test
          public void test1() {
      //        allMatch(Predicate p)——检查是否匹配所有元素,返回boolean类型
              // 练习:是否所有员工年龄都大于18
              List<Employee> employees = EmployeeData.getEmployees();
              System.out.println(employees.stream().allMatch(e -> e.getAge() > 18));
      
      //         anyMatch(Predicate p)——检查是否至少匹配一个元素,返回boolean类型
              // 练习:是否存在员工年龄大于18的
              System.out.println(employees.stream().anyMatch(e -> e.getAge() > 18));
      
      //        findFirst——返回第一个元素
              System.out.println(employees.stream().findFirst());  // Optional[Employee{id=1001, name='马化腾', age=34, salary=10000}]
              System.out.println(employees.stream().findFirst().get());  // Employee{id=1001, name='马化腾', age=34, salary=10000}
      
          }
      
          @Test
          public void test2() {
      //        count()——返回流中元素的总个数
              List<Employee> employees = EmployeeData.getEmployees();
              System.out.println(employees.stream().filter(employee -> employee.getSalary() > 7000).count());
      
      //        max(Comparator c)——返回流中最大值
              System.out.println(employees.stream().max((e1, e2) -> Integer.compare(e1.getSalary(), e2.getSalary())));
              /*
                  Optional[Employee{id=1003, name='刘强东', age=45, salary=20000}]
               */
              // 返回最高的工资
              // 方式1
              System.out.println(employees.stream().max((e1, e2) -> Integer.compare(e1.getSalary(), e2.getSalary())).get().getSalary());
              // 方式2:映射
              System.out.println(employees.stream().map(e -> e.getSalary()).max(Integer::compareTo));  // Optional[20000]
      
      
      //        min(Comparator c)——返回流中最小值
              System.out.println(employees.stream().min((e1, e2) -> Integer.compare(e1.getSalary(), e2.getSalary())));
              /*
                  Optional[Employee{id=1002, name='马云', age=2, salary=5000}]
               */
      
      //        forEach(Consumer c)——遍历元素,执行操作
              employees.stream().forEach(System.out::println);
              /*
                  Employee{id=1001, name='马化腾', age=34, salary=10000}
                  Employee{id=1002, name='马云', age=2, salary=5000}
                  Employee{id=1003, name='刘强东', age=45, salary=20000}
               */
      
              // 针对集合,jdk8中增加了一个遍历的方法
              employees.forEach(System.out::println);
          }
      
    2. 归约

          // 2. 归约
          @Test
          public void test3() {
      //        reduce(T identity, BinaryOperator b)——可以将流中元素反复结合起来,得到一个值。返回T
              // 练习1:计算1-10的自然数的和
              List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
              System.out.println(list.stream().reduce(0, (x, y) -> x + y));  // 55
              System.out.println(list.stream().reduce(10, (x, y) -> x + y));  // 65
              System.out.println(list.stream().reduce(0, Integer::sum));  // 55
      
      //        reduce(BinaryOperator b)——可以将流中元素反复结合起来,得到一个值。返回Optional<T>
              // 练习2:计算公司所有员工工资的总和
              List<Employee> employees = EmployeeData.getEmployees();
              System.out.println(employees.stream().map(e -> e.getSalary()).reduce(0, (s1, s2) -> Integer.sum(s1, s2)));  // 35000
              System.out.println(employees.stream().map(Employee::getSalary).reduce(0, Integer::sum));  // 35000
          }
      
    3. 收集

          // 3. 收集
          @Test
          public void test4() {
              List<Employee> employees = EmployeeData.getEmployees();
      //      collect(Collector c)——将流转换为其他形式。接收一个Collector接口的实现,用于给Stream中元素做汇总的方法
              // 练习1:查找员工工资大于6000的员工,结果返回为一个List或Set
              List<Employee> list1 = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toList());
              employees.forEach(System.out::println);
              /*
                  Employee{id=1001, name='马化腾', age=34, salary=10000}
                  Employee{id=1002, name='马云', age=2, salary=5000}
                  Employee{id=1003, name='刘强东', age=45, salary=20000}
               */
              System.out.println();
              list1.forEach(System.out::println);
              /*
                  Employee{id=1001, name='马化腾', age=34, salary=10000}Employee{id=1001, name='马化腾', age=34, salary=10000}
                  Employee{id=1003, name='刘强东', age=45, salary=20000}
               */
      
              System.out.println();
              // 练习2:按照员工的年龄进行排序,返回一个新的List中
              List<Employee> list2 = employees.stream().sorted((e1, e2) -> Integer.compare(e1.getAge(), e2.getAge())).collect(Collectors.toList());
              list2.forEach(System.out::println);
              /*
                  Employee{id=1002, name='马云', age=2, salary=5000}
                  Employee{id=1001, name='马化腾', age=34, salary=10000}
                  Employee{id=1003, name='刘强东', age=45, salary=20000}
               */
          }
      

反射

入门案例:通过反射调用方法

@SuppressWarnings("all")
public class ReflectionQuestion {

    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 传统的方式 new -> 调用方法
//        Cat cat = new Cat();
//        cat.hi();

        // 尝试反射
        // 1. 使用Properties类,可以读写配置文件
        Properties properties = new Properties();
        properties.load(new FileInputStream("src\\re.properties"));
        String classfullpath = properties.get("classfullpath").toString();
        String methodName = properties.get("method").toString();
        System.out.println("classfullpath = " + classfullpath);  // classfullpath = com.honvin.reflection.Cat
        System.out.println("method = " + methodName);  // method = hi

        // 2. 创建对象,传统方法不行
//        new classfullpath();	

        // 3. 使用反射机制
        // (1) 加载类,返回Class类型的对象
        Class clazz = Class.forName(classfullpath);
        // (2) 通过 clazz 得到加载的类 com.honvin.reflection.Cat 的对象实例
        Object o = clazz.getDeclaredConstructor().newInstance();
        System.out.println("o的运行类型: " + o.getClass());
        // (3) 通过 clazz 得到加载的类 com.honvin.reflection.Cat 的 methodName=“hi” 方法的对象
        //     即:在反射中,可以把方法视为对象
        Method method1 = clazz.getMethod(methodName);
        // (4) 通过 method1 调用方法,即通过方法对象来实现调用方法
        System.out.println("===== 反射机制 =====");
        method1.invoke(o);  // 传统方法: 对象.方法(); 反射机制:方法.invoke(对象);
    }
}
  1. 反射机制允许程序在执行期借助于ReflectionAPI取得任何类的内部信息(比如成员变量,构造器,成员方法等等),并能操作对象的属性及方法。反射在设计模式和框架底层都会用到
  2. 加载完类之后,在堆中就产生了一个class类型的对象(一个类只有一个Class对象),这个对象包含了类的完整结构信息。通过这个对象得到类的结构。这个Class对象就像一面镜子,透过这个镜子看到类的结构,所以,形象的称之为:反射

image-20231204170359043

  • Java反射机制可以完成

    • 在运行时判断任意一个对象所属的类
    • 在运行时构造任意一个类的对象
    • 在运行时得到任意一个类所具有的成员变量和方法
    • 在运行时调用任意一个对象的成员变量和方法
    • 生成动态代理
  • 反射相关的主要类:

    • java.lang.Class:代表一个类,Class对象表示某个类加载后在堆中的对象
    • java.lang.reflect.Method:代表类的方法,Method对象表示某个类的方法
    • java.lang.reflect.Field:代表类的成员变量,Field对象表示某个类的成员变量
    • java.lang.reflect.Constructor:代表类的构造方法,Constructor对象表示构造器
    @SuppressWarnings("all")
    public class Reflection01 {
    
        public static void main(String[] args) throws Exception {
            // 使用Properties类,可以读写配置文件
            Properties properties = new Properties();
            properties.load(new FileInputStream("src\\re.properties"));
            String classfullpath = properties.get("classfullpath").toString();
            String methodName = properties.get("method").toString();
            // (1) 加载类,返回Class类型的对象
            Class clazz = Class.forName(classfullpath);
            // (2) 通过 clazz 得到加载的类 com.honvin.reflection.Cat 的对象实例
            Object o = clazz.getDeclaredConstructor().newInstance();
            System.out.println("o的运行类型: " + o.getClass());
            // (3) 通过 clazz 得到加载的类 com.honvin.reflection.Cat 的 methodName=“hi” 方法的对象
            //     即:在反射中,可以把方法视为对象
            Method method1 = clazz.getMethod(methodName);
            // (4) 通过 method1 调用方法,即通过方法对象来实现调用方法
            System.out.println("===== 反射机制 =====");
            method1.invoke(o);  // 传统方法: 对象.方法(); 反射机制:方法.invoke(对象);
    
            // java.lang.reflect.Field (getField不能得到私有属性)
            Field nameField = clazz.getField("age");
            System.out.println(nameField.get(o));  // 传统方法: 对象.属性; 反射机制:属性.get(对象);
    
            // java.lang.reflect.Constructor
            Constructor constructor = clazz.getConstructor();  // ()可以指定参数类型,空代表无参构造器
            System.out.println(constructor);  // public com.honvin.reflection.Cat()
    
            Constructor constructor1 = clazz.getConstructor(String.class);  // 这里传入的 String.classs 就是String类的class对象
            System.out.println(constructor1);  // public com.honvin.reflection.Cat(java.lang.String)
    
        }
    }
    
  • 反射优点和缺点

    • 优点:可以动态的创建和使用对象(也是框架底层核心),使用灵活,没有反射机制,框架技术就失去底层支撑。
    • 缺点:使用反射基本是解释执行,对执行速度有影响。
  • 反射调用优化-关闭访问检查

    • Method和Field、Constructor对象都有setAccessibleO方法
    • setAccessible作用是启动和禁用访问安全检查的开关
    • 参数值为true表示反射的对象在使用时取消访问检查,提高反射的效率。参数值为false则表示反射的对象执行访问检查
Class类
  • 基本介绍

    1. Class也是类,因此也继承object类

      image-20231204182332587

    2. Class类对象不是new出来的,而是系统创建的

    3. 对于某个类的Class类对象,在内存中只有一份,因为类只加载一次

    4. 每个类的实例都会记得自己是由哪个Class实例所生成

    5. 通过Class可以完整地得到一个类的完整结构,通过一系列API

      image-20231204183058598

    6. Class对象是存放在堆的

    7. 类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据(包括方法代码)变量名,方法名,访问权限等等)https://www.zhihu.com/question/38496907

  • Class类常用的方法

    public class Class01 {
        public static void main(String[] args) throws Exception {
            String classAllPath = "com.honvin.reflection.Car";
            // 1. 获取到Car类对应的Class对象
            // <?> 表示不确定的Java类型
            Class<?> cls = Class.forName(classAllPath);
            // 2.输出 cls
            System.out.println(cls);  // 显示cls对象,输出的是哪个类的Class对象 class com.honvin.reflection.Car
            System.out.println(cls.getClass());  // 输出cls的运行类型class java.lang.Class
            // 3. 得到包名
            System.out.println(cls.getPackage().getName());  // com.honvin.reflection
            // 4. 得到类名
            System.out.println(cls.getName());  // com.honvin.reflection.Car
            System.out.println(cls.getSimpleName());  // Car
            // 5. 通过cls创建对象实例
            Car car = (Car) cls.getConstructor().newInstance();
            System.out.println(car.toString());  // Car{brand='BMW', price=10000000, color='black'}
            // 6. 通过反射获取属性 brand
            Field brand = cls.getField("brand");
            System.out.println(brand.get(car));  // BMW
            // 7. 通过反射给属性赋值
            brand.set(car, "Audi");
            System.out.println(brand.get(car));  // Audi
            // 8. 通过遍历得到所有的属性(字段)
            System.out.println("===== 所有的属性(字段) =====");
            Field[] fields = cls.getFields();
            for (Field field : fields) {
                System.out.println(field.getName());  // 字段名称
            }
        }
    }
    
  • 获取Class类对象

    1. 前提:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forNameO获取,可能抛出ClassNotFoundException,实例:Class cls1=Class.forName( “java.lang.Cat”);

      应用场景:多用于配置文件,读取类全路径,加载类.

    2. 前提:若已知具体的类,通过类的class获取,该方式最为安全可靠,程序性能最高实例:Classcls2=Cat.class;

      应用场景:多用于参数传递,比如通过反射得到对应构造器对象。

    3. 前提:已知某个类的实例,调用该实例的getClassO方法获取Class对象,实例:Classclazz=对象.getClassO://运行类型

      应用场景:通过创建好的对象,获取Class对象。

    4. 其他方式ClassLoader cl =对象.getClassO.getClassLoaderO;Class clazz4=cl.loadClass(“类的全类名”);

    5. 基本数据(int,char,boolean,float,double,byte,long,short)按如下方式得到Class类对象

      Class cls=基本数据类型.class

    6. 基本数据类型对应的包装类,可以通过.TYPE 得到Class类对象

      Class cls =包装类.TYPE

    public class GetClass_ {
        public static void main(String[] args) throws Exception {
    
            // 1. Class.forName
            String classAllPath = "com.honvin.reflection.Car";  // 通过配置文件获取
            Class<?> cls1 = Class.forName(classAllPath);
            System.out.println(cls1);  // class com.honvin.reflection.Car
    
            // 2. 类名.class;应用场景:多用于参数传递
            Class cls2 = Car.class;
            System.out.println(cls2);  // class com.honvin.reflection.Car
    
            // 3. 对象.getClass()
            Car car = new Car();
            Class cls3 = car.getClass();
            System.out.println(cls3);  // class com.honvin.reflection.Car
    
            // 4. 通过类加载器【4种】来获取到类的Class对象
            // (1) 先得到类加载器 car
            ClassLoader classLoader = car.getClass().getClassLoader();
            // (2) 通过类加载器得到Class对象
            Class<?> cls4 = classLoader.loadClass(classAllPath);
            System.out.println(cls4);  // class com.honvin.reflection.Car
    
            // cls1,cls2,cls3,cls4 其实是同一个Class对象,hashcode相同
    
            // 5. 基本数据(int,char,boolean,float,double,byte,long,short)按如下方式得到Class类对象
            Class<Integer> integerClass = int.class;
            Class<Character> characterClass = char.class;
            System.out.println(integerClass + "\t" + characterClass);  // int   char
    
            // 6. 基本数据类型对应的包装类,可以通过.TYPE 得到Class类对象
            Class<Integer> integerClass1 = Integer.TYPE;
            Class<Character> characterClass1 = Character.TYPE;
            System.out.println(integerClass1 + "\t" + characterClass1);  // int char
    
            // integerClass,integerClass1 的 hashcode相同
    
        }
    }
    
  • 哪些类有Class对象

    1. 外部类,成员内部类,静态内部类,局部内部类,匿名内部类
    2. interface:接口
    3. 数组
    4. enum:枚举
    5. annotation:注解
    6. 基本数据类型
    7. void
类加载

基本说明:

反射机制是java实现动态语言的关键,也就是通过反射实现类动态加载。

  1. 静态加载:编译时加载相关的类,如果没有则报错,依赖性太强

  2. 动态加载:运行时加载需要的类,如果运行时不用该类,则不报错,降低了依赖性

  3. 类加载时机

    1. 当创建对象时(new)//静态加载
    2. 当子类被加载时,父类也加载 //静态加载
    3. 调用类中的静态成员时 //静态加载
    4. 通过反射 //动态加载

类加载过程图:

image-20231204191635006

类加载各阶段完成任务:

image-20231204191815924

  • 加载阶段

    JVM在该阶段的主要目的是将字节码从不同的数据源(可能是class文件、也可能是jar包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的java.lang.Class对象。

  • 连接阶段-验证

    1. 目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
    2. 包括:文件格式验证(是否以魔数oxcafebabe开头)、元数据验证、字节码验证和符号引用验证。
    3. 可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间。
  • 连接阶段-准备

    JVM会在该阶段对静态变量,分配内存并默认初始化(对应数据类型的默认初始值,如0、0L、null、false等)。这些变量所使用的内存都将在方法区中进行分配

    public int n1 = 10;
    // n1是实例属性,不是静态变量,因此在准备阶段,是不会分配内存
    public static int n2 = 20;
    // n2是静态变量,分配内存n2是默认初始化0,而不是20
    public static final int n3 = 30;
    // n3是static final是常量,他和静态变量不一样,因为一旦赋值就不变 n3=30
    
  • 连接阶段-解析

    虚拟机将常量池内的符号引用替换为直接引用的过程

  • 初始化

    1. 到初始化阶段,才真正开始执行类中定义的Java程序代码,此阶段是执行()方法的过程。

    2. ()方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并。

    3. 虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 ()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕

      正因为有这个机制,才能保证某个类在内存中只有一份Class对象。

通过反射获取类的结构信息

第一组:java.lang.Class类

  1. getName:获取全类名
  2. getSimpleName:获取简单类名
  3. getFields:获取所有public修饰的属性,包含本类以及父类的
  4. getDeclaredFields:获取本类中所有属性
  5. getMethods:获取所有public修饰的方法,包含本类以及父类的
  6. getDeclaredMethods:获取本类中所有方法
  7. getConstructors:获取所有public修饰的构造器,包含本类
  8. getDeclaredConstructors:获取本类中所有构造器
  9. getPackage:以Package形式返回包信息
  10. getSuperClass:以Class形式返回父类信息
  11. getlnterfaces:以Class[]形式返回接口信息
  12. getAnnotations:以Annotation[]形式返回注解信息

第二组:java.lang.reflect.Field类

  1. getModifiers:以int形式返回修饰符

    [说明:默认修饰符是0,public是1,private是2,protected是4,static是8,final是16]

  2. getType:以Class形式返回类型3.getName:返回属性名

第三组:java.lang.reflect.Method类

  1. getModifiers:以int形式返回修饰符

    [说明:默认修饰符是0,public是1,private是2,protected是4,static是8,final是16]

  2. getReturnType:以Class形式获取返回类型

  3. getName:返回方法名

  4. getParameterTypes:以Class[]返回参数类型数组

第四组:java.lang.reflect.Constructor类

  1. getModifiers:以int形式返回修饰符
  2. getName:返回构造器名名(全类名)
  3. getParameterTypes:以class[]返回参数类型数组
通过反射创建对象
  1. 方式一:调用类中的public修饰的无参构造器
  2. 方式二:调用类中的指定构造器
  3. Class类相关方法
    1. newlnstance:调用类中的无参构造器,获取对应类的对象
    2. getConstructor(Class…clazz):根据参数列表,获取对应的public构造器对象
    3. getDecalaredConstructor(Class…clazz):根据参数列表,获取对应的构造器对象
  4. Constructor类相关方法
    1. setAccessible:暴破
    2. newlnstance(Object…obj):调用构造器
public class ReflectionCreateInstance {
    public static void main(String[] args) throws Exception {
        // 1. 先获取到 User 类的Class对象
        Class<?> userClass = Class.forName("com.honvin.reflection.User");

        // 2. 通过public的无参构造器
        Object o = userClass.getConstructor().newInstance();
        System.out.println(o);  // User{age=10, name='Honvin'}

        // 3. 通过public的有参构造器的创建实例
        /*
            constructor:
            public User(String name) {
                this.name = name;
            }
         */
        // 3.1 先得到构造器
        Constructor<?> constructor = userClass.getConstructor(String.class);
        // 3.2 创建实例,并传入实参
        Object zhw = constructor.newInstance("zhw");
        System.out.println(zhw);  // User{age=10, name='zhw'}

        // 4. 通过非public的有参构造器创建实例
        Constructor<?> constructor1 = userClass.getDeclaredConstructor(int.class, String.class);
        constructor1.setAccessible(true);  // 爆破【暴力破解】,使用反射可以访问 private 构造器/方法/属性
        Object honvin = constructor1.newInstance(20, "Honvin");
        System.out.println(honvin);  // User{age=20, name='Honvin'}
    }
}

class User {
    private int age = 10;
    private String name = "Honvin";

    public User() {
    }

    public User(String name) {
        this.name = name;
    }

    private User(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}
通过反射访问类中的成员
  1. 根据属性名获取Field对象

    Field f= clazz.getDeclaredField(属性名);

  2. 爆破:f.setAccessible(true); 其中 f 是 Field

  3. 访问

    f.set(o.值); // o 表示对象

    syso(f.get(o));

  4. 如果是静态属性,则set和get中的参数o,可以写成null

public class ReflectionAccessProperty {
    public static void main(String[] args) throws Exception {
        // 1. 得到Student类队以哦那个的Class对象
        Class<?> stuClass = Class.forName("com.honvin.reflection.Student");
        // 2. 创建对象
        Object o = stuClass.getConstructor().newInstance();
        // 3。 反射操作得到 age 属性对象
        Field age = stuClass.getField("age");
        age.set(o, 10);
        System.out.println(o);

        // 使用反射操作 name 属性(私有静态)
        Field name = stuClass.getDeclaredField("name");
        name.setAccessible(true);  // 设置为true【爆破】,表示忽略掉访问权限控制
//        name.set(o, "Honvin");
        name.set(null, "zhw");  // 因为那么是static的,因此o也可以写成null
        System.out.println(o);
        System.out.println(name.get(o));
        System.out.println(name.get(null));  // 要求name是static的

    }
}

class Student {
    public int age;
    private static String name;

    public Student() {
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name +
                '}';
    }
}
通过反射访问类中的方法
  1. 根据方法名和参数列表获取Method方法对象:

    Methodm=clazz.getDeclaredMethod(方法名,XX.class) //得到本类的所有方法

  2. 获取对象:objecto=clazz.newlnstance();

  3. 暴破:m.setAccessible(true);

  4. 访问:ObjectreturnValue=m.invoke(o,实参列表);

  5. 注意:如果是静态方法,则invoke的参数o,可以写成null!

public class ReflectionAccessMethod {
    public static void main(String[] args) throws Exception {
        // 1. 得到Student类队以哦那个的Class对象
        Class<?> bossClass = Class.forName("com.honvin.reflection.Boss");
        // 2. 创建对象
        Object o = bossClass.getConstructor().newInstance();
        // 3. 调用 public 的hi方法
//        Method hi = bossClass.getMethod("hi", String.class);
        // 或者
        // 3.1 得到hi方法对象
        Method hi = bossClass.getDeclaredMethod("hi", String.class);
        hi.invoke(o, "honvin");  // hi honvin

        // 4. 调用 private static 的 say 方法
        // 4.1 得到 say 方法对象(私有静态)
        Method say = bossClass.getDeclaredMethod("say", int.class, String.class, char.class);
        // 4.2 因为 say 方法是 private,所以需要爆破
        say.setAccessible(true);
        System.out.println(say.invoke(0, 100, "张三", '男'));  // 100 张三 男
        
        // 5. 在反射中,如果方法有返回值,同意返回Object,但是他运行类型和方法定义的返回类型一致
        Object invoke = say.invoke(0, 100, "张三", '男');
        System.out.println(invoke.getClass());  // class java.lang.String
    }
}

class Boss {
    public int age;
    private static String name;

    public Boss() {
    }

    public static String say(int n, String s, char c) {  // 私有的静态方法
        return n + " "  + s + " " + c;
    }

    public void hi(String s) {  // 普通方法
        System.out.println("hi " + s);
    }
}

正则表达式

正则表达式初体验

public class Regexp_ {
    public static void main(String[] args) {
        //假定,编写了爬虫,从百度页面得到如下文本
        String content = "1995年,互联网的蓬勃发展给了Oak机会。业界为了使死板、单调的" +
                "静态网页能够“灵活”起来,急需一种软件技术来开发一种程序,这种程序可以通" +
                "过网络传播并且能够跨平台运行。于是,世界各大IT企业为此纷纷投入了大量的" +
                "人力、物力和财力。这个时候,Sun公司想起了那个被搁置起来很久的Oak,并且" +
                "重新审视了那个用软件编写的试验平台,由于它是按照嵌入式系统硬件平台体系结" +
                "构进行编写的,所以非常小,特别适用于网络上的传输系统,而Oak也是一种精简的" +
                "语言,程序非常小,适合在网络上传输。Sun公司首先推出了可以嵌入网页并且可以" +
                "随同网页在网络上传输的Applet(Applet是一种将小程序嵌入到网页中进行执行的技术)," +
                "并将Oak更名为Java(在申请注册商标时,发现Oak已经被人使用了,再想了一系列" +
                "名字之后,最终,使用了提议者在喝一杯Java咖啡时无意提到的Java词" +
                "语)。5月23日,Sun公司在Sun world会议上正式发" +
                "布Java和HotJava浏览器。IBM、Apple、DEC、Adobe、HP、Oracle、Netscape和微软" +
                "等各大公司都纷纷停止了自己的相关开发项目,竞相购买了Java使用许可证,并为自己的产" +
                "品开发了相应的Java平台";

        // 提取文章中所有的英文单词
        // (1). 传统方法,使用遍历方式,代码量大,效率不高
        // (2). 正则表达式技术

        // 1. 先创建一个 Pattern 对象,模式对象,可以理解成一个正则表达式对象
//        Pattern pattern = Pattern.compile("[a-zA-Z]+");  //  英文单词
        Pattern pattern = Pattern.compile("[0-9]+");  //  数字
        // 2. 创建一个匹配器对象
        Matcher matcher = pattern.matcher(content);
        // 3。可以开始循环匹配
        while (matcher.find()) {  // 匹配到了返回true
            // 匹配内容,文本,放到 m.group(0) 方法中
            System.out.println(matcher.group(0));
        }
    }
}

正则表达式是处理文本的利器

正则表达式是对字符串执行模式匹配的技术。

正则表达式:regular expression => Regexp

正则表达式基本介绍

  1. 一个正则表达式,就是用某种模式去匹配字符串的一个公式。很多人因为它们看上去比较古怪而且复杂所以不敢去使用,不过,经过练习后,就觉得这些复杂的表达式写起来还是相当简单的,而且,一旦你弄懂它们,你就能把数小时辛苦而且易错的文本处理工作缩短在几分钟(甚至几秒钟)内完成。

  2. 特别强调,正则表达式不是只有java才有,实际上很多编程语言都支持正则表达式进行字符串操作!

  3. 基本步骤:

    // 待匹配的字符串
    String content = "xxxxxx";
    // 1. 书写正则表达式 \\d 表示一个任意数字
    String regStr = "\\d\\d\\d\\d";
    // 2. 创建模式对象[即正则表达式对象]
    Pattern pattern = Pattern.compile(regStr);
    // 3. 创建匹配器
    // 说明:创建匹配器 matcher,按照 正则表达式的股则 去匹配 content 字符串
    Matcher matcher = pattern.matcher(content);
    // 4. 开始匹配
    while (matcher.find()) {
        System.out.println("找到 " + matcher.group(0));
    }
    

正则表达式的底层实现

public class RegTheory {
    public static void main(String[] args) {
        String content = "1998年12月8日,第二代Java平台的企业版J2EE发布。1999年6月,Sun公司发布了" +
                "第二代Java平台(简称为Java2)的3个版本:J2ME(Java2 Micro Edition,Java2平台的微型" +
                "版),应用于移动、无线及有限资源的环境;J2SE(Java 2 Standard Edition,Java 2平台的" +
                "标准版),应用于桌面环境;J2EE(Java 2Enterprise Edition,Java 2平台的企业版),应" +
                "用3443于基于Java的应用服务器。Java 2平台的发布,是Java发展过程中最重要的一个" +
                "里程碑,标志着Java的应用开始普及9889 ";
        // 目标:匹配所有四个数字
        // 说明
        // 1. \\d 表示一个任意数字
//        String regStr = "\\d\\d\\d\\d";
        String regStr = "(\\d\\d)(\\d\\d)";  // 分组
        // 2. 创建模式对象[即正则表达式对象]
        Pattern pattern = Pattern.compile(regStr);
        // 3. 创建匹配器
        // 说明:创建匹配器 matcher,按照 正则表达式的股则 去匹配 content 字符串
        Matcher matcher = pattern.matcher(content);
        // 4. 开始匹配
        /**
         * matcher.find() 完成的任务(考虑分组)
         * *什么时分组:把正则表达式中用括号括起来的部分,如 String regStr = "(\\d\\d)(\\d\\d)";,叫做分组
         *  第一个 () 表示第一组,第二个 () 表示第二组
         * 1. 根据指定的规则,定位满足规则的子字符串(比如 1998)
         * 2. 找到后,
         *    将子字符串开始的索引记录到matcher对象的属性 int[] groups; 的groups[0]中
         *      groups[0] 表示子字符串开始的索引
         *    将子字符串结束的索引+1 记录到matcher对象的属性 int[] groups; 的groups[1]中
         *      groups[1] 表示子字符串结束的索引+1
         *    将第1组()匹配到的 字符串开始的索引 记录到groups[2]中,将第1组()匹配到的 字符串结束的索引+1 记录到groups[3]中。
         *    将第2组()匹配到的 字符串开始的索引 记录到groups[4]中,将第1组()匹配到的 字符串结束的索引+1 记录到groups[5]中。
         *    如果有更多的分组,依次类推
         * 3. 同时记录 oldLast 的值为 子字符串的结束的索引+1 的值,下次执行find时,就从 oldLast 开始匹配
         * <p>
         *
         * matcher.group(0) 分析
         * 源码:
         * public String group(int group) {
         *         checkMatch();
         *         checkGroup(group);
         *         if ((groups[group*2] == -1) || (groups[group*2+1] == -1))
         *             return null;
         *         return getSubSequence(groups[group * 2], groups[group * 2 + 1]).toString();
         *     }
         * 1. 根据 group[0] 和 group[1] 的记录的位置,从 content 开始获取子字符串返回
         *    返回的子字符串,包含子字符串开始的位置 group[0],不包含子字符串结束的索引+1 group[1]
         */
        while (matcher.find()) {
            System.out.println("找到 " + matcher.group(0));
        }
    }
}

image-20231218202505908

小结:

  • 如果正则表达式有(),即有分组
  • 取出匹配的字符串的规则如下
    1. group(0) 表示匹配到的子字符串
    2. group(1) 表示匹配到的子字符串的第一组子串
    3. group(2) 表示匹配到的子字符串的第二组子串
    4. … 但是分组的数不能越界。

正则表达式语法

元字符-转义符

元字符(Metacharacter)——转义符 \\

\\符号说明:我们使用正则表达式去检索某些特殊字符的时候,需要用到转义符号,否则检索不到结果,甚至会报错的。案例:用 去匹配“ a b c 去匹配“abc 去匹配abc(”会怎样?

用(去匹配“abc$(”会怎样?

注意:在 Java 的正则表达式中,两个\(即\\)表示其它语言中的一个\

  • 需要用到转义符号的字符:. * + ( ) $ / \ ? [ ] ^ { }
public class RegExp02 {
    public static void main(String[] args) {
        String content = "abc$(ab.c(123(";
        // 匹配(
//        String regex = "\\(";
        String regex = "\\.";  // 匹配.
        Pattern pattern = Pattern.compile(regex);
        Matcher matches = pattern.matcher(content);
        while (matches.find()) {
            System.out.println(matches.group(0));
        }
    }
}
元字符-字符匹配符

image-20231218203831774

image-20231218203926068

  • [a-z] 表示可以匹配a-z中任意一个
    • java正则表达式默认是区分字母大小写的,如何实现不区分大小写?
      • (?i)abc 表示abc都不区分大小写
      • a(?i)bc 表示bc不区分大小写
      • a((?i)b)c 表示只有b不区分大小写
      • Pattern pat = Pattern.compile(regExp, Pattern.CASE_INSENSITIVE);
  • [A-Z] 表示可以匹配A-Z中任意一个字符。
  • [0-9] 表示可以匹配0-9中任意一个字符。
  • [^a-z]说明:[^a-z]表示可以匹配不是 a-z 中的任意一个字符
  • [^A-Z]说明:[^A-Z]表示可以匹配不是 a-z 中的任意一个字符
  • [^0-9]说明:[^a-z]表示可以匹配不是 a-z 中的任意一个字符
  • [abcd]表示可以匹配 abcd 中的任意一个字符。
  • [^abcd]表示可以匹配不是 abcd 中的任意一个字符。
  • \\d表示可以匹配 0-9 的任意一个数字,相当于[0-9]。
  • \\D表示可以匹配不是0-9中的任意一个数字,相当于[^0-9]
  • \\w匹配任意英文字符、数字和下划线,相当于 [a-zA-Z0-9]
  • \\W相当于[^a-zA-Z0-9]\\w刚好相反
  • \\s匹配任何空白字符(空格,制表符等)
  • \\S匹配任何非空白字符,和\\s刚好相反
  • .匹配除 \n 之外的所有字符,如果要匹配.本身则需要使用\\.
元字符-选择匹配符

在匹配某个字符串的时候是选择性的,即:既可以匹配这个,又可以匹配那个,这时需要用到选择匹配符号 |

元字符-限定符

用于指定其前面的字符和组合项连续出现多少次。

image-20231218210000563

image-20231218210030433

元字符-定位符

定位符, 规定要匹配的字符串出现的位置,比如在字符串的开始还是在结束的位置,这个也是相当有用的,必须掌握。

image-20231218210243656

分组

捕获分组:

image-20231218210639929

public class RegExp04 {
    public static void main(String[] args) {
        String content = "hanshunping s7789 nn1189han";
        // 下面是非命名分组
        // 说明:
        // 1. matcher.group(0) 得到匹配整个正则表达式的字符串
        // 2. matcher.group(1)  得到匹配第一个小括号里(第1组)的字符串
        // 3. matcher.group(2)  得到匹配第二个小括号里(第2组)的字符串
//        String regex = "(\\d\\d)(\\d\\d)";  // 匹配四个数字的字符串

        // 命名分组:即可以给分组取名
        String regex = "(?<g1>\\d\\d)(?<g2>\\d\\d)";  // 匹配四个数字的字符串

        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(content);
        while (matcher.find()){
            System.out.println("得到匹配整个正则表达式的字符串 " + matcher.group(0));  // 得到匹配整个正则表达式的字符串 7789
            System.out.println("第一个小括号里(第1组)的字符串 " + matcher.group(1));  // 第一个小括号里(第1组)的字符串 77
            System.out.println("第二个小括号里(第2组)的字符串 " + matcher.group(2));  // 第二个小括号里(第2组)的字符串 89

            System.out.println("第一个小括号里(第1组)的字符串[通过组名] " + matcher.group("g1"));  // 第一个小括号里(第1组)的字符串[通过组名] 77
            System.out.println("第二个小括号里(第2组)的字符串[通过组名] " + matcher.group("g2"));  // 第二个小括号里(第2组)的字符串[通过组名] 89
        }
    }
}

非捕获分组:

image-20231218211605974

public class RegExp05 {
    public static void main(String[] args) {
        String content = "hello韩顺平教育 jack韩顺平老师 韩顺平同学hello韩顺平学生";

        // 找到 韩顺平教育 、韩顺平老师、韩顺平同学 子字符串
//        String regex = "韩顺平教育|韩顺平老师|韩顺平同学";
//        String regex = "韩顺平(教育|老师|同学)";  // 还是捕获分组,使用 matcher.group(1)能查到
        //上面的写法可以等价非捕获分组, 注意:不能 matcher.group(1),因为并不是分组,不能捕获
//        String regex = "韩顺平(?:教育|老师|同学)";

        //找到 韩顺平 这个关键字,但是要求只是查找韩顺平教育和 韩顺平老师 中包含有的韩顺平
        //下面也是非捕获分组,不能使用 matcher.group(1)
//        String regex = "韩顺平(?=教育|老师)";

        //找到 韩顺平 这个关键字,但是要求只是查找 不是 (韩顺平教育 和 韩顺平老师) 中包含有的韩顺平
        //下面也是非捕获分组,不能使用 matcher.group(1)
        String regex = "韩顺平(?!教育|老师)";

        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(content);
        while (matcher.find()){
            System.out.println(matcher.group(0));
        }

    }
}
非贪婪匹配

?

public class RegExp09 {
    public static void main(String[] args) {
        String content = "hello111111 ok";
        //String regStr = "\\d+"; //默认是贪婪匹配 111111
       // String regStr = "\\d+?"; //非贪婪匹配 1 1 1 1 1 1
        String regStr = "\\d+?"; //非贪婪匹配

        Pattern pattern = Pattern.compile(regStr);
        Matcher matcher = pattern.matcher(content);
        while (matcher.find()) {
            System.out.println("找到: " + matcher.group(0));
        }
    }
}
案例-验证url
public class RegExp06 {
    public static void main(String[] args) {
        String content = "https://github.com/timerring/backend-tutorial/blob/main/java-tutorial/ch27_regular_expressions.md";
//        String content = "https://github.com";

        /**
         * 思路:
         * 1. 先确定 url 的开头部分 https:// | http://
         * 2. 然后通过 ([\w-]+\.)+[\w-]+ 匹配 github.com
         * 3. /timerring/backend-tutorial/blob/main/java-tutorial/ch27_regular_expressions.md
         *
         */
        String regex = "^((https|http)://)([\\w-]+\\.)+[\\w-]+(/[\\w-?=&/%.#]*)?$";  // 注意:[.]表示匹配.本身

        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(content);
        if (matcher.find()){
            System.out.println("满足格式");
        } else {
            System.out.println("不满足格式");
        }
    }
}

正则表达式三个常用类

java.util.regex包主要包括以下三个类 Pattern 类、Matcher 类和 PatternSyntaxException

  • Pattern类 pattern对象是一个正则表达式对象。Pattern类没有公共构造方法。要创建一个 Pattern对象,调用其公共静态方法,它返回一个 Pattern对象。该方法接受一个正则表达式作为它的第一个参数,比如:Pattern r= Pattern.compile(pattern);

    • matches由于是整体匹配,所以甚至可以不用定位符^$。默认就是整体匹配。

      用于整体匹配, 在验证输入的字符串是否满足条件使用。

      下面是使用Pattern.matches进行url验证:

      public class RegExp06 {
          public static void main(String[] args) {
              String content = "https://github.com/timerring/backend-tutorial/blob/main/java-tutorial/ch27_regular_expressions.md";
      //        String content = "https://github.com";
      
              /**
               * 思路:
               * 1. 先确定 url 的开头部分 https:// | http://
               * 2. 然后通过 ([\w-]+\.)+[\w-]+ 匹配 github.com
               * 3. /timerring/backend-tutorial/blob/main/java-tutorial/ch27_regular_expressions.md
               *
               */
              String regex = "^((https|http)://)([\\w-]+\\.)+[\\w-]+(/[\\w-?=&/%.#]*)?$";  // 注意:[.]表示匹配.本身
              String regex2 = "((https|http)://)([\\w-]+\\.)+[\\w-]+(/[\\w-?=&/%.#]*)?";  // 注意:可以不适用限定符
      
              Pattern pattern = Pattern.compile(regex);
              Matcher matcher = pattern.matcher(content);
              if (matcher.find()){
                  System.out.println("满足格式");
              } else {
                  System.out.println("不满足格式");
              }
      
              boolean pattern2 = Pattern.matches(regex2, content);
              System.out.println(pattern2);  // true
          }
      }
      
  • Matcher类 Matcher对象是对输入字符串进行解释和匹配的引擎。与Pattern类一样,Matcher 也没有公共构造方法。你需要调用Pattern对象的matcher方 法来获得一个Matcher对象

    image-20231219102001417

    image-20231219102349522

    public class RegExp07 {
        public static void main(String[] args) {
            String str = "hello honvin, hello world.";
            String regex = "hello";
            Pattern pattern = Pattern.compile(regex);
            Matcher matcher = pattern.matcher(str);
            while (matcher.find()) {
                System.out.println("===============");
                System.out.println("start:" + matcher.start());  // 相当于 group[0]
                System.out.println("end:" + matcher.end());  // 相当于 group[1]
                System.out.println("找到 " + str.substring(matcher.start(), matcher.end()));  // 找到 hello
            }
    
            //整体匹配方法,常用于,去校验某个字符串是否满足某个规则
            System.out.println("整体匹配=" + matcher.matches());  // 整体匹配=false
    
            // 完成如果str有honvin,就替换成zhw
            regex = "honvin";
            pattern = Pattern.compile(regex);
            matcher = pattern.matcher(str);
            // 注意:返回的字符串才是替换后的字符串 原来的 str 不变化
            String content = matcher.replaceAll("zhw");
            System.out.println("替换前="+ str);  // 替换前=hello honvin, hello world.
            System.out.println("替换后=" + content);  // 替换后=hello zhw, hello world.
        }
    }
    
  • PatternSyntaxException PatternSyntaxException是一个非强制异常类,它表示一个正则表达式模式中的语法错误。

分组、捕获、反向引用

提出需求

请看下面问题:

给你一段文本,请你找出所有四个数字连在一起的子串,并且这四个数字要满足

① 第1位与第4位相同

② 第2位与第3位相同,比如1221 , 5775 ,…

介绍

(\\dd) (\\dd)

要解决前面的问题,我们需要了解正则表达式的几个概念;

  1. 分组:我们可以用圆括号组成一个比较复杂的匹配模式,那么一个圆括号的部分我们可以看作是一个子表达式/一个分组。
  2. 捕获:把正则表达式中子表达式/分组匹配的内容,保存到内存中以数字编号或显式命名的组里,方便后面引用,从左向右,以分组的左括号为标志,第一个出现的分组的组号为1,第二个为2,以此类推。组O代表的是整个正则式。
  3. 反向引用:圆括号的内容被捕获后。可以在这个括号后被使用,从而写出一个比较实用的匹配模式,这个我们称为反向引用,这种引用既可以是在正则表达式内部,也可以是在正则表达式外部,内部反向引用\\分组号,外部反向引用$分组号 (即不在正则表达式内部使用,见结巴案例)。
看几个小案例
  1. 要匹配两个连续的相同数字:(\\d)\\1\\1表示引用分组1的结果
  2. 要匹配五个连续的相同数字:(\\d)\\1{4}
  3. 要匹配个位与千位相同,十位与百位相同的数5225,1551 (\\d)(\\d)\\2\\1\\2表示引用分组2的结果,\\1表示引用分组1的结果。

请在字符串中检索商品编号,形式如:12321-333999111 这样的号码,要求满足

​ 前面是一个五位数,且是回文数

​ 然后一个-号

​ 然后是一个九位数,连续的每三位要相同

package com.honvin.regexp;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @ClassName: RegExp08
 * @Date: 2023/12/19 10:39
 * @Author: Honvin
 * @Software: IntelliJ IDEA
 * @Description: 请在字符串中检索商品编号,形式如:12321-333999111 这样的号码
 *               要求满足:
 *                  前面是一个五位数,且是回文数
 *                  然后一个-号
 *                  然后是一个九位数,连续的每三位要相同
 **/
public class RegExp08 {
    public static void main(String[] args) {
        String content = "12321-333999111";
        String regex = "^(\\d)(\\d)\\d\\2\\1-(\\d)\\3{2}(\\d)\\4{2}(\\d)\\5{2}$";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(content);
        while (matcher.find()) {
            System.out.println("找到 " + matcher.group(0));
        }
    }
}
  • 经典的结巴程序

    把类似: “我…我要…学学学学…编程java!”; 通过正则表达式修改成"我要学编程java"

    package com.honvin.regexp;
    
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    /**
     * @ClassName: RegExp09
     * @Date: 2023/12/19 10:49
     * @Author: Honvin
     * @Software: IntelliJ IDEA
     * @Description: 把类似: "我....我要....学学学学....编程java!"; 通过正则表达式修改成"我要学编程java"
     **/
    public class RegExp09 {
        public static void main(String[] args) {
            String str = "我....我要....学学学学....编程java!";
            // 1. 去掉所有的 .
            Pattern pattern = Pattern.compile("\\.");
            Matcher matcher = pattern.matcher(str);
            str = matcher.replaceAll("");
            System.out.println(str);  // 我我要学学学学编程java!
    
            // 2. 去掉重复的字
            // 思路
            // (1) 使用 (.)\\1+ 匹配重复的字
            // (2) 使用 反向引用 $1 来替换匹配到的内容
            pattern = Pattern.compile("(.)\\1+");  // 分组的捕获内容记录到 $1
            matcher = pattern.matcher(str);
            while (matcher.find()) {
                System.out.println("找到 " + matcher.group(0));
            }
            // 使用 反向引用 $1 来替换匹配到的内容
            str = matcher.replaceAll("$1");  // 使用捕获到的分组的内容替换所有匹配到的内容
            System.out.println(str);  // 我要学编程java!
            
            // 3. 使用一条语句实现去重
            String content = Pattern.compile("(.)\\1+").matcher(str).replaceAll("$1");
            System.out.println(content);  // 我要学编程java!
        }
    }
    

String 类中使用正则表达式

替换功能
Stringpublic String replaceAll(String regex, String replacement)
判断功能
Stringpublic boolean matches(String regex){} 
//使用Pattern 和 Matcher 类
分割功能
Stringpublic String[] split(String regex)
案例
public class StringReg {
    public static void main(String[] args) {
        String content = "2000年5月,JDK1.3、JDK1.4和J2SE1.3相继发布,几周后其" +
                "获得了Apple公司Mac OS X的工业标准的支持。2001年9月24日,J2EE1.3发" +
                "布。" +
                "2002年2月26日,J2SE1.4发布。自此Java的计算能力有了大幅提升";

        //使用正则表达式方式,将 JDK1.3 和 JDK1.4 替换成JDK
        content = content.replaceAll("JDK1\\.3|JDK1\\.4", "JDK");
        System.out.println(content);  // 2000年5月,JDK、JDK和J2SE1.3相继发布,几周后其获得了Apple公司Mac OS X的工业标准的支持。2001年9月24日,J2EE1.3发布。2002年2月26日,J2SE1.4发布。自此Java的计算能力有了大幅提升

        //要求 验证一个 手机号, 要求必须是以 138 139 开头的
        content = "13832445666";
        if (content.matches("1(38|39)\\d{8}")) {
            System.out.println("手机号验证通过");
        } else {
            System.out.println("手机号验证不通过");
        }

        //要求按照 # 或者 - 或者 ~ 或者 数字 来分割
        System.out.println("===================");
        content = "hello#abc-jack12smith~北京";
        String[] split = content.split("#|-|~|\\d+");
        for (String s : split) {
            System.out.println(s);
        }
    }
}

设计模式

单例设计模式

  • 定义:所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

  • 单例模式有两种方式:

    1. 饿汉式(有可能用不到这个对象,但是在类中已经创建好了,可能造成创建了对象,但是没有使用)

      1. 构造器私有化 =》防止直接 new

      2. 类的内部创建对象(该对象是static)

      3. 向外暴露一个静态的公共方法。getInstance

        public class SingleTon {
            public static void main(String[] args) {
                Girlfriend instance = Girlfriend.getInstance();
                System.out.println(instance);
            }
        }
        
        // 有一个类,Girlfriend
        // 只能有一个女朋友
        class Girlfriend {
            private String name;
        
            // 如何保障我们只能创建一个 GirTFriend 对象
            // 步骤
            // 1. 将构造器私有化
            // 2. 在类的内部直接创建
            // 3. 提供一个公共的static方法,返回 gf对象
            private Girlfriend(String name) {
                this.name = name;
            }
        
            private static Girlfriend gf = new Girlfriend("wang");
        
            public static Girlfriend getInstance() {
                return gf;
            }
        
            @Override
            public String toString() {
                return "Girlfriend{" +
                        "name='" + name + '\'' +
                        '}';
            }
        }
        

        image-20231021211719513

    2. 懒汉式(存在线程安全问题)

      1. 将构造器私有化

      2. 定义一个static属性对象

      3. 提供一个public的static方法,可以返回一个该对象

      4. 只有当用户使用getInstance时,才返回gf对象,后面再次调用时,会返回上次创建的gf对象

        public class SingleTon {
            public static void main(String[] args) {
                Girlfriend instance = Girlfriend.getInstance();
                System.out.println(instance);
            }
        }
        
        // 有一个类,Girlfriend
        // 只能有一个女朋友
        class Girlfriend {
            private String name;
        
            // 如何保障我们只能创建一个 GirTFriend 对象
            private Girlfriend(String name) {
                this.name = name;
            }
        
            private static Girlfriend gf;
        
            public static Girlfriend getInstance() {
                if(gf == null) {  // 如果还没有创建对象
        			gf = new Girlfriend("wang");
                }
                return gf;
            }
        
            @Override
            public String toString() {
                return "Girlfriend{" +
                        "name='" + name + '\'' +
                        '}';
            }
        }
        
  • 饿汉式 VS 懒汉式

    1. 二者最主要的区别在于创建对象的时机不同: 饿汉式是在类加载就创建了对象实例而懒汉式是在使用时才创建
    2. 饿汉式不存在线程安全问题懒汉式存在线程安全问题。(后面学习线程后,会完善)
    3. 饿汉式存在浪费资源的可能。因为如果程序员一个对象实例都没有使用,那么饿汉式创建的对象就浪费了,懒汉式是使用时才创建,就不存在这个问题。
    4. 在我们javaSE标准类中,java.lang.Runtime就是经典的单例模式

java事件处理机制

基本介绍

  • java事件处理是采取"委派事件模型"。当事件发生时,产生事件的对像,会把此"信息"传递给"事件的监听者"处理,这里所说的"信息"实际上就是java.awt.event事件类库里某个类所创建的对象,把它称为"事件的对象”。

    image-20231112165303837

  • public class Event implements KeyListener {
        // 有字符输出时触发
        @Override
        public void keyTyped(KeyEvent e) {
    
        }
    
        // 当某个键按下时触发
        @Override
        public void keyPressed(KeyEvent e) {
            System.out.println((char) e.getKeyCode() + " was pressed.");
        }
    
        // 当某个键释放(松开)时触发
        @Override
        public void keyReleased(KeyEvent e) {
    
        }
    }
    

深入理解

  • 事件源:事件源是一个产生事件的对像,比如按钮,窗口等。

  • 事件:事件就是承载事件源状态改变时的对象,比如当键盘事件、鼠标事件、窗口事件等等,会生成一个事件对象,该对像保存着当前事件很多信息,比如KeyEvent对象有含有被按下键的Code值。java.awt.event包和javax.swing.event包中定义了各种事件类型。

  • 事件类型:

    image-20231112165115177

  • 事件监听器接口

    1. 当事件源产生一个事件,可以传送给事件监听者处理;
    2. 事件监听者实际上就是一个类,该类实现了某个事件监听器接口比如前面我们案例中的MyPanle就是一个类,它实现了KeyListener?接口,它就可以作为一个事件监听者,对接受到的事件进行处理;
    3. 事件监听器接口有多种,不同的事件监听器接口可以监听不同的事件,一个类可以实现多个监听接口
    4. 这些接口在java.awt.event包和javax.swing.event包中定义。列出常用的事件监听器接口,查看jdk文档聚集了。

项目开发流程

image-20231127113821563

  1. 需求分析

    1. 需求分析师:情懂技术+行业
    2. 出一个需求分析报告(白皮书),该项目功能.客户具体要求
    3. 时间:30%
  2. 设计阶段

    1. 结构式 / 项目经理
    2. 设计工作(UML类图,流程图,模块设计,数据库,架构)
    3. 原型开发
    4. 组建团队
    5. 时间:20%
  3. 实现阶段

    1. 程序员 / 码农
    2. 完成架构师的模块功能
    3. 测试自己模块
    4. 时间:20%
  4. 测试阶段

    1. 测试工程师

    2. 单元测试。测试用例

      白盒测试,黑盒测试,集成测试

    3. 时间:20%

  5. 实施阶段

    1. 实施工程师(开发能力不高 / 环境配置部署能力)
    2. 项目正确的部署到客户的平台,并保证运行正常
    3. 身体好(出差)
    4. 时间:10%
  6. 维护阶段

    1. 发现bug解决
      2002年2月26日,J2SE1.4发布。自此Java的计算能力有了大幅提升

      //要求 验证一个 手机号, 要求必须是以 138 139 开头的
      content = “13832445666”;
      if (content.matches(“1(38|39)\d{8}”)) {
      System.out.println(“手机号验证通过”);
      } else {
      System.out.println(“手机号验证不通过”);
      }

      //要求按照 # 或者 - 或者 ~ 或者 数字 来分割
      System.out.println(“===================”);
      content = “hello#abc-jack12smith~北京”;
      String[] split = content.split(“#|-|~|\d+”);
      for (String s : split) {
      System.out.println(s);
      }
      }
      }


------

## 设计模式

### 单例设计模式

* 定义:所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

* 单例模式有两种方式:

  1. 饿汉式(有可能用不到这个对象,但是在类中已经创建好了,可能造成创建了对象,但是没有使用)

     1. 构造器私有化 =》防止直接 new

     2. 类的内部创建对象(该对象是static)

     3. 向外暴露一个静态的公共方法。getInstance

        ```java
        public class SingleTon {
            public static void main(String[] args) {
                Girlfriend instance = Girlfriend.getInstance();
                System.out.println(instance);
            }
        }
        
        // 有一个类,Girlfriend
        // 只能有一个女朋友
        class Girlfriend {
            private String name;
        
            // 如何保障我们只能创建一个 GirTFriend 对象
            // 步骤
            // 1. 将构造器私有化
            // 2. 在类的内部直接创建
            // 3. 提供一个公共的static方法,返回 gf对象
            private Girlfriend(String name) {
                this.name = name;
            }
        
            private static Girlfriend gf = new Girlfriend("wang");
        
            public static Girlfriend getInstance() {
                return gf;
            }
        
            @Override
            public String toString() {
                return "Girlfriend{" +
                        "name='" + name + '\'' +
                        '}';
            }
        }
        ```

        [外链图片转存中...(img-ok35N69r-1702982906910)]

  2. 懒汉式(存在线程安全问题)

     1. 将构造器私有化

     2. 定义一个static属性对象

     3. 提供一个public的static方法,可以返回一个该对象

     4. 只有当用户使用getInstance时,才返回gf对象,后面再次调用时,会返回上次创建的gf对象

        ```java
        public class SingleTon {
            public static void main(String[] args) {
                Girlfriend instance = Girlfriend.getInstance();
                System.out.println(instance);
            }
        }
        
        // 有一个类,Girlfriend
        // 只能有一个女朋友
        class Girlfriend {
            private String name;
        
            // 如何保障我们只能创建一个 GirTFriend 对象
            private Girlfriend(String name) {
                this.name = name;
            }
        
            private static Girlfriend gf;
        
            public static Girlfriend getInstance() {
                if(gf == null) {  // 如果还没有创建对象
        			gf = new Girlfriend("wang");
                }
                return gf;
            }
        
            @Override
            public String toString() {
                return "Girlfriend{" +
                        "name='" + name + '\'' +
                        '}';
            }
        }
        ```

        

* 饿汉式 VS 懒汉式

  1. 二者最主要的区别在于创建对象的时机不同: 饿汉式是在类加载就创建了对象实例而懒汉式是在使用时才创建
  2. 饿汉式不存在线程安全问题懒汉式存在线程安全问题。(后面学习线程后,会完善)
  3. 饿汉式存在浪费资源的可能。因为如果程序员一个对象实例都没有使用,那么饿汉式创建的对象就浪费了,懒汉式是使用时才创建,就不存在这个问题。
  4. 在我们javaSE标准类中,java.lang.Runtime就是经典的单例模式

## java事件处理机制

基本介绍

* java事件处理是采取"委派事件模型"。当事件发生时,产生事件的对像,会把此"信息"传递给"事件的监听者"处理,这里所说的"信息"实际上就是java.awt.event事件类库里某个类所创建的对象,把它称为"事件的对象”。

  [外链图片转存中...(img-WMZoeW1D-1702982906910)]

* ```java
  public class Event implements KeyListener {
      // 有字符输出时触发
      @Override
      public void keyTyped(KeyEvent e) {
  
      }
  
      // 当某个键按下时触发
      @Override
      public void keyPressed(KeyEvent e) {
          System.out.println((char) e.getKeyCode() + " was pressed.");
      }
  
      // 当某个键释放(松开)时触发
      @Override
      public void keyReleased(KeyEvent e) {
  
      }
  }

深入理解

  • 事件源:事件源是一个产生事件的对像,比如按钮,窗口等。

  • 事件:事件就是承载事件源状态改变时的对象,比如当键盘事件、鼠标事件、窗口事件等等,会生成一个事件对象,该对像保存着当前事件很多信息,比如KeyEvent对象有含有被按下键的Code值。java.awt.event包和javax.swing.event包中定义了各种事件类型。

  • 事件类型:

    [外链图片转存中…(img-5HdQWpCn-1702982906910)]

  • 事件监听器接口

    1. 当事件源产生一个事件,可以传送给事件监听者处理;
    2. 事件监听者实际上就是一个类,该类实现了某个事件监听器接口比如前面我们案例中的MyPanle就是一个类,它实现了KeyListener?接口,它就可以作为一个事件监听者,对接受到的事件进行处理;
    3. 事件监听器接口有多种,不同的事件监听器接口可以监听不同的事件,一个类可以实现多个监听接口
    4. 这些接口在java.awt.event包和javax.swing.event包中定义。列出常用的事件监听器接口,查看jdk文档聚集了。

项目开发流程

[外链图片转存中…(img-Km4Qv1LQ-1702982906910)]

  1. 需求分析

    1. 需求分析师:情懂技术+行业
    2. 出一个需求分析报告(白皮书),该项目功能.客户具体要求
    3. 时间:30%
  2. 设计阶段

    1. 结构式 / 项目经理
    2. 设计工作(UML类图,流程图,模块设计,数据库,架构)
    3. 原型开发
    4. 组建团队
    5. 时间:20%
  3. 实现阶段

    1. 程序员 / 码农
    2. 完成架构师的模块功能
    3. 测试自己模块
    4. 时间:20%
  4. 测试阶段

    1. 测试工程师

    2. 单元测试。测试用例

      白盒测试,黑盒测试,集成测试

    3. 时间:20%

  5. 实施阶段

    1. 实施工程师(开发能力不高 / 环境配置部署能力)
    2. 项目正确的部署到客户的平台,并保证运行正常
    3. 身体好(出差)
    4. 时间:10%
  6. 维护阶段

    1. 发现bug解决
    2. 项目升级
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值