Java基础篇

一、Statis关键字

成员变量有2种
– 有static修饰的属于类叫静态成员变量,与类一起加载一次,直接用类名调用即可。
– 无static修饰的属于类的每个对象的叫实例成员变量, 与类的对象一起加载,对象有多少个,实例成员变量就加载多少份。必须用类的对象调用。

成员方法有2种:
– 有static修饰的属于类叫静态方法,直接用类名调用即可。
– 无static修饰的属于类的每个对象的叫实例方法,必须用类的对象调用。

8种访问形式的问答:
a.实例方法是否可以直接访问实例成员变量? 可以的,因为它们都属于对象。
b.实例方法是否可以直接访问静态成员变量? 可以的,静态成员变量可以被共享访问。
c.实例方法是否可以直接访问实例方法? 可以的,实例方法和实例方法都属于对象。

​ d.实例方法是否可以直接访问静态方法? 可以的,静态方法可以被共享访问!

​ a.静态方法是否可以直接访问实例变量? 不可以的,实例变量必须用对象访问!!
​ b.静态方法是否可以直接访问静态变量? 可以的,静态成员变量可以被共享访问。
​ c.静态方法是否可以直接访问实例方法? 不可以的,实例方法必须用对象访问!!
​ d.静态方法是否可以直接访问静态方法? 可以的,静态方法可以被共享访问!!

二、继承

2.1 概述

继承的作用?
“可以提高代码的复用”,相同代码可以定义在父类中。
然后子类直接继承父类,就可以直接使用父类的这些代码了。
(相同代码重复利用)

子类更强大:子类不仅得到了父类的功能,它还有自己的功能。

继承的特点:
子类继承了一个父类,子类就可以直接得到父类的属性(成员变量)和行为(方法)了。

继承的格式:
子类 extends 父类

2.2 成员变量

this代表了当前对象的引用,可以用于访问当前子类对象的成员变量。
super代表了父类对象的引用,可以用于访问父类中的成员变量。

子类访问成员变量的原则:就近原则。
如果一定要访问父类的成员变量可以使用super关键字

2.3 成员方法

子类对象优先使用子类已有的方法。

2.4 方法重写

如果觉得父类方法不好用,则进行重写,加上注解

@Override

Java建议在重写的方法上面加上一个@Override注解。
方法一旦加了这个注解,那就必须是成功重写父类的方法,否则报错!
@Override优势:可读性好,安全,优雅!!

方法重写的具体要求:
    1.子类重写方法的名称和形参列表必须与父类被重写方法一样。
    2.子类重写方法的返回值类型申明要么与父类一样,要么比父类方法返回值类型范围更小。(以后再了解)
    3.子类重写方法的修饰符权限应该与父类被重写方法的修饰符权限相同或者更大。(以后再了解)
    4.子类重写方法申明抛出的异常应该与父类被重写方法申明抛出的异常一样或者范围更小!(以后再了解)

2.5 继承构造器

子类的全部构造器默认一定会先访问父类的无参数构造器,再执行子类自己的构造器。

1.子类的构造器的第一行默认有一个super()调用父类的无参数构造器,写不写都存在!
2.子类继承父类,子类就得到了父类的属性和行为。
当我们调用子类构造器初始化子类对象数据的时候,必须先调用父类构造器初始化继承自父类的属性和行为啊。

2.6 继承的特点

1.单继承:一个类只能继承一个直接父类。
为什么Java是单继承的?
答:反证法,假如Java可以多继承,请看如下代码:
class A{
public void test(){
System.out.println(“A”);
}
}
class B{
public void test(){
System.out.println(“B”);
}
}
class C extends A , B {
public static void main(String[] args){
C c = new C();
c.test(); // 出现了类的二义性!所以Java不能多继承!!
}
}

2.多层继承:一个类可以间接继承多个父类。(家谱)
3.一个类可以有多个子类。
4.一个类要么默认继承了Object类,要么间接继承了Object类,Object类是Java中的祖宗类!!

2.7 引用类型

复合变量:

private String id;
private String name;
private double x;
private double y;
private Integer id;
private String name;
private Address address;
Students students = new Students();

students.setId(1);
students.setName("mohan");
Address address = new Address("haha", "haha", 101, 101);

students.setAddress(address);
System.out.println(students.getAddress().getId());

三、抽象类

抽象类只是为了约束子类去重写他的方法,子类必须重写所有约束类的方法

抽象类有普通类的所有方法,属性,一模一样,但是就是不能被调用实例化。

Earth earth = new Earth();  //报错

抽象类存在的意义有两点:

​ (1)被继承,抽象类就是为了被子类继承,否则抽象类将毫无意义。(核心意义)

​ (2)抽象类体现的是"模板思想":部分实现,部分抽象。(拓展)
​ – 可以使用抽象类设计一个模板模式。

3.1 抽象类设计模板模式

3.1.1 什么是设计模式

  • 设计模式是前任或者技术大牛或者软件行业实战中发现的精良软件架构和思想

  • 后来者可以直接用这些精良软件架构和思想,提高效率、提高软件可扩展行、可维护性

​ 抽象类是部分实现,部分抽象的含义,所以可以设计模板模式。
​ 好处:模板可以确定的模板自己实现,模板不能确定的定义成抽象方法交给使用模板的人重写。
​ 可以设计出优秀的设计模式,提升开发效率,提高代码的重用性!

3.2 注意事项

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。

理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。

 2. 抽象类一定有而且是必须有构造器,是供子类创建对象时,初始化父类成员使用的。
理解:子类的构造器中,有默认的super(),需要访问父类构造器。
  1. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

    1. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则子类也必须定义成抽象类。

    2. 抽象类存在的意义是为了被子类继承,抽象类体现的是模板思想。
      理解:抽象类中已经实现的是模板中确定的成员,
      抽象类不确定如何实现的定义成抽象方法,交给具体的子类去实现。

四、接口

4.1 概述

我们已经学完了抽象类,抽象类中可以用抽象方法,也可以有普通方法,已经构造器,成员变量等。那么什么是接口呢?接口是更加彻底的抽象,接口中全部是抽象方法。(JDK8之前),接口同样是不能创建对象的

4.2 定义格式

//接口的定义格式:
修饰符 interface 接口名称{
    // 抽象方法
}

// 修饰符:public|缺省
// 接口的声明:interface
// 接口名称:首字母大写,满足“驼峰模式”

4.3 接口成分的特点

在JDK8之前,接口中的成分包含:抽象方法和常量

4.3.1.抽象方法

​ 注意:接口中的抽象方法默认会自动加上public abstract修饰程序员无需自己手写!!
​ 按照规范:以后接口中的抽象方法建议不要写上public abstract。因为没有必要啊,默认会加上。

4.3.2 常量

在接口中定义的成员变量默认会加上: public static final修饰。也就是说在接口中定义的成员变量实际上是一个常量。这里是使用public static final修饰后,变量值就不可被修改,并且是静态化的变量可以直接用接口名访问,所以也叫常量。常量必须要给初始值。常量命名规范建议字母全部大写,多个单词用下划线连接。

4.3.3 案例演示

public interface InterF {
    // 抽象方法!
    //    public abstract void run();
    void run();

    //    public abstract String getName();
    String getName();

    //    public abstract int add(int a , int b);
    int add(int a , int b);


    // 它的最终写法是:
    // public static final int AGE = 12 ;
    int AGE  = 12; //常量
    String SCHOOL_NAME = "黑马程序员";

}

4.4 基本的实现

4.4.1 实现接口的概述

类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口的子类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements关键字。

4.4.2 实现接口的格式

/**接口的实现:
    在Java中接口是被实现的,实现接口的类称为实现类。
    实现类的格式:*/
[修饰符] class 类名 implements 接口1,接口2,接口3...{


}

从上面格式可以看出,接口是可以被多实现的。大家可以想一想为什么呢?

4.4.3 类实现接口的要求和意义

  1. 必须重写实现的全部接口中所有抽象方法。
  2. 如果一个类实现了接口,但是没有重写完全部接口的全部抽象方法,这个类也必须定义成抽象类。
  3. 意义:接口体现的是一种规范,接口对实现类是一种强制性的约束,要么全部完成接口申明的功能,要么自己也定义成抽象类。这正是一种强制性的规范。

4.4.4 类与接口基本实现案例

假如我们定义一个运动员的接口(规范),代码如下:

/**
   接口:接口体现的是规范。
 * */
public interface SportMan {
    void run(); // 抽象方法,跑步。
    void law(); // 抽象方法,遵守法律。
    String compittion(String project);  // 抽象方法,比赛。
}

接下来定义一个乒乓球运动员类,实现接口,实现接口的实现类代码如下:

package com.itheima._03接口的实现;
/**
 * 接口的实现:
 *    在Java中接口是被实现的,实现接口的类称为实现类。
 *    实现类的格式:
 *      [修饰符] class 类名 implements 接口1,接口2,接口3...{
 *
 *
 *      }
 * */
public class PingPongMan  implements SportMan {
    @Override
    public void run() {
        System.out.println("乒乓球运动员稍微跑一下!!");
    }

    @Override
    public void law() {
        System.out.println("乒乓球运动员守法!");
    }

    @Override
    public String compittion(String project) {
        return "参加"+project+"得金牌!";
    }
}

测试代码

public class TestMain {
    public static void main(String[] args) {
        // 创建实现类对象。
        PingPongMan zjk = new PingPongMan();
        zjk.run();
        zjk.law();
        System.out.println(zjk.compittion("全球乒乓球比赛"));

    }
}

4.4.5 类与接口的多实现案例

类与接口之间的关系是多实现的,一个类可以同时实现多个接口。

首先我们先定义两个接口,代码如下:

/** 法律规范:接口*/
public interface Law {
    void rule();
}

/** 这一个运动员的规范:接口*/
public interface SportMan {
    void run();
}

然后定义一个实现类:

/**
 * Java中接口是可以被多实现的:
 *    一个类可以实现多个接口: Law ,SportMan
 *
 * */
public class JumpMan implements Law ,SportMan {
    @Override
    public void rule() {
        System.out.println("尊长守法");
    }

    @Override
    public void run() {
        System.out.println("训练跑步!");
    }
}

从上面可以看出类与接口之间是可以多实现的,我们可以理解成实现多个规范,这是合理的。

4.5 接口与接口的多继承

Java中,接口与接口之间是可以多继承的:也就是一个接口可以同时继承多个接口。大家一定要注意:

类与接口是实现关系

接口与接口是继承关系

接口继承接口就是把其他接口的抽象方法与本接口进行了合并。

案例演示:

public interface Abc {
    void go();
    void test();
}

/** 法律规范:接口*/
public interface Law {
    void rule();
    void test();
}

 *
 *  总结:
 *     接口与类之间是多实现的。
 *     接口与接口之间是多继承的。
 * */
public interface SportMan extends Law , Abc {
    void run();
}

4.6 JDK 8之后的接口新增方法

从JDK 8开始之后,接口不再纯洁了,接口中不再只是抽象方法,接口还可以有默认方法(也就是实例方法),和静态方法了,还包含了私有实例方法和私有静态方法

4.6.1 含有默认方法和静态方法

默认方法:使用 default 修饰,不可省略,供子类调用或者子类重写。

静态方法:使用 static 修饰,供接口直接调用。

代码如下:

public interface InterFaceName {
    public default void method() {
        // 执行语句
    }
    public static void method2() {
        // 执行语句    
    }
}

4.6.2 含有私有方法和私有静态方法

私有方法:使用 private 修饰,供接口中的默认方法或者静态方法调用。

代码如下:

public interface InterFaceName {
    private void method() {
        // 执行语句
    }
}

4.6.3 新增方法的使用

默认方法和静态方法以及私有方法和私有静态方法遵循面向对象的继承关系使用原则,实现类依然可以访问接口的非私有方法,对于接口中的非私有静态方法,可以直接通过接口名进行访问。

重写默认方法注意(了解):

  • 子接口重写默认方法时,default关键字可以保留。

  • 实现类重写默认方法时,default关键字不可以保留。

4.7 实现多个接口使用注意事项

4.7.1 多个接口同名静态方法

如果实现了多个接口,多个接口中存在同名的静态方法并不会冲突,原因是只能通过各自接口名访问静态方法。

public interface A {
  public static void test(){

  }
}

 interface B {
    public static void test(){

    }
}

class C implements  A , B{
    public static void main(String[] args) {
        People.test();
        B.test();
       // C.test(); // 编译出错
    }
}

4.7.2 优先级的问题

当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类就近选择执行父类的成员方法。代码如下:

定义接口:

interface A {
    public default void methodA(){
        System.out.println("AAAAAAAAAAAA");
    }
}

定义父类:

class D {
    public void methodA(){
        System.out.println("DDDDDDDDDDDD");
    }
}

定义子类:

class C extends D implements A {
  	// 未重写methodA方法
}

定义测试类:

public class Test {
    public static void main(String[] args) {
        C c = new C();
        c.methodA(); 
    }
}
输出结果:
DDDDDDDDDDDD

4.8 接口小结

  • 接口中,无法定义成员变量,但是可以定义常量,其值不可以改变,默认使用public static final修饰。
  • 接口中的方法全是抽象方法,默认会自动加上public abstract修饰
  • JDK 8开始,接口不再纯洁,支持静态方法,默认方法,私有方法。
  • 接口中,没有构造器,不能创建对象
  • 类与接口是多实现的
  • 接口与接口是多继承的
  • 接口体现的规范。

五、 代码块

5.1 引入

类的成分:
​ 1.成员变量
​ 2.构造器
​ 3.成员方法
​ 4.代码块
​ 5.内部类

我们已经学完了成员变量,构造器,成员方法,接下来我们来介绍以下代码快,代码块按照有无static可以分为静态代码块和实例代码块。

5.2 静态代码块

静态代码块
​ 必须有static修饰,必须放在类下。与类一起加载执行。

格式

static{
     // 执行代码
}

特点

  • 每次执行类,加载类的时候都会先执行静态代码块一次。
  • 静态代码块是自动触发执行的,只要程序启动静态代码块就会先执行一次。
  • 作用:在启动程序之前可以做资源的初始化,一般用于初始化静态资源。

案例演示

public class DaimaKuaiDemo01 {
    public static String sc_name ;

    // 1.静态代码块
    static {
        // 初始化静态资源
        sc_name = "黑马程序员!";
        System.out.println("静态代码块执行!");
    }

    public static void main(String[] args) {
        System.out.println("main方法执行");
        System.out.println(sc_name);
    }
}

5.3 实例代码块

实例代码块
​ 没有static修饰,必须放在类下。与对象初始化一起加载。

格式

{
     // 执行代码
}

特点

  • 无static修饰。属于对象,与对象的创建一起执行的。
  • 每次调用构造器初始化对象,实例代码块都要自动触发执行一次。
  • 实例代码块实际上是提取到每一个构造器中去执行的。
  • 作用:实例代码块用于初始化对象的资源。

案例演示

public class DaimaKuaiDemo02 {
   
    private String name ;

    // 实例代码块。 无static修饰。
    {
        System.out.println("实例代码块执行");
        name = "dl";
    }

    // 构造器
    public DaimaKuaiDemo02(){
        //System.out.println("实例代码块执行");
    }

    // 有参数构造器
    public DaimaKuaiDemo02(String name){
        //System.out.println("实例代码块执行");
    }

    public static void main(String[] args) {
        // 匿名对象,创建出来没有给变量。
        new DaimaKuaiDemo02();
        new DaimaKuaiDemo02();
        new DaimaKuaiDemo02("xulei");
    }
}
// 输出三次:实例代码块执行

常用API

六、单例设计模式

6.1 概论

单例的意思是一个类只存在一个对象,不能创建多个对象

6.2 为什么要使用单例

开发中有很多类的对象我们只需要一个,例如虚拟机对象!人文管理系对象

对象越多约占内存,有时候我们只需要一个对象就可以实现业务,单例可以节省内存

6.3 如何实现单例

单例的实现有两种

  1. 饿汉单例设计模式:

    通过类获取单例对象的时候,对象已经提前做好了!

    实现步骤:

    • 创建构造器

      private SingleInstance() {
      
        }
      
    • 定义静态成员变量

       private static SingleInstance ins = new SingleInstance();
      
    • 定义静态成员方法

       public static SingleInstance getInstance() {
          return ins;
        }
      
    • 用类直接进行调用

      SingleInstance instance1 = SingleInstance.getInstance();
      SingleInstance instance2 = SingleInstance.getInstance();
      System.out.println(instance1.equals(instance2));
      
  2. 懒汉单例设计模式:

    ​ 等到需要用的时候,在进行创建

    ​ 实现步骤:

    • 创建类

    • 创建私有构造函数

       private SingleInstance() {
      
        }
      
    • 创建静态成员变量

      private static SingleInstance02 ins;
      
    • 定义静态成员变量

      public static SingleInstance02 getInstance() {
          if (ins == null){
            ins = new SingleInstance02();
          }
          return ins;
        }
      
    • 调用

       SingleInstance02 s1  = SingleInstance02.getInstance();
          SingleInstance02 s2  = SingleInstance02.getInstance();
          System.out.println(s1.equals(s2));
      

七、枚举

枚举类用于做信息标志和信息分类

public class EnumDemo02 {
  public static void main(String[] args) {
    move(Orditation.UP);
  }

  public static void move(Orditation orditation) {
    switch (orditation){
      case UP -> System.out.println(orditation.UP);
      case DOWN -> System.out.println(orditation.DOWN);
      case LEFT -> System.out.println(orditation.LEFT);
      case RIGHT -> System.out.println(orditation.RIGHT);
    }
  }
}
enum Orditation{
  UP,DOWN,LEFT,RIGHT
}

八、多态

8.1 概述

多态的形式:
父类类型 对象名称 = new 子类构造器;
接口 对象名称 = new 实现类构造器;

父类类型的范围 > 子类类型范围的。

多态的概念:
同一个类型的对象,执行同一个行为,在不同的状态下会表现出不同的行为特征。

多态的识别技巧:

​ 对于方法的调用:编译看左边,运行看右边。

​ 对于变量的调用:编译看左边,运行看左边。

8.2 多态使用

 Animals bsd = new Cat();

    Cat c1 =(Cat) bsd;			//将bsd转换成真的猫
    
    c1.catch1();

8.3 多态综合使用

(1)提供2个USB设备。(USB设备必须满足:接入和拔出的功能)*         
(2)定义一个USB的接口(申明USB设备的规范必须是:实现接入和拔出的功能)*         
(3)开始定义2个真实的实现类代表鼠标标和键盘*         
(4)定义一个电脑类。
public class DemoPloy {
  public static void main(String[] args) {
    Computer computer = new Computer();
    Usb mymouse = new KeyBoard("新键盘");
    computer.InsertUsb(mymouse);
  }
}

class Computer {
  public void InsertUsb(Usb usb){
    usb.insert();

    if (usb instanceof Mouse){
      Mouse mouse1 = (Mouse) usb;
      mouse1.DoubleMouse();
    }else if (usb instanceof KeyBoard){
      KeyBoard keyBoard = (KeyBoard) usb;
      keyBoard.KeyDown();
    }

    usb.unconnect();
  }
}

class Mouse implements Usb{
  private String name;

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

  public void DoubleMouse() {
    System.out.println("双击了666");
  }

  @Override
  public void insert() {
    System.out.println("鼠标插入");
  }

  @Override
  public void unconnect() {
    System.out.println("鼠标拔出");
  }
}

class KeyBoard implements Usb{
  private String name;

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

  public void KeyDown(){
    System.out.println("老铁摁下了键盘666");
  }

  @Override
  public void insert() {
    System.out.println("键盘插入");
  }

  @Override
  public void unconnect() {
    System.out.println("键盘拔出");
  }
}


interface Usb{
 void insert();
  void unconnect();
}

九、内部类

内部类是类的五大成分之一:成员变量,方法,构造器,代码区,内部类

9.1 概述

定义在一个类里边的类就是内部类

有什么用?

可以更好的封装、

9.2 静态内部类

静态内部类属于外部类本身,只会加载一次
所以它的特点与外部类是完全一样的,只是位置在别人里面而已。

9.3 实例内部类

实例内部类属于外部类对象,需要用外部类对象一起加载,
实例内部类可以访问外部类的全部成员!

9.4 局部内部类

局部内部类没啥用。

9.5 匿名内部类

1.匿名内部类是一个没有名字的内部类。
2.匿名内部类一旦写出来,就会立即创建一个匿名内部类的对象返回。
3.匿名内部类的对象的类型相当于是当前new的那个的类型的子类类型。

 Swim bozai = new Swim() {
            @Override
            public void swimming() {
                System.out.println("老师🏊‍的贼溜~~~~");
            }
        };

十、包和权限修饰符

相同包下的类可以直接访问。
不同包下的类必须导包,才可以使用!
导包格式:import 包名.类名;

四种修饰符的访问权限范围:
private 缺省 protected public
本类中 √ √ √ √
本包下其他类中 X √ √ √
其他包下的类中 X X X √
其他包下的子类中 X X √ √

十一、Object类和Date日期类

11.1 Object概述

包:java.lang.Object
Object类是Java中的祖宗类。
一个类要么默认继承了Object类,要么间接继承了Object类。
Object类的方法是一切子类都可以直接使用的,所以我们要学习Object类的方法。

11.2 Object类的常用方法

(1)public String toString():

默认是返回当前对象在堆内存中的地址信息:

(2)public boolean equals(Object o):

equals存在的意义是为了被子类重写,以便程序员可以
自己来定制比较规则。

11.3 Objects 的方法

1.public static boolean equals(Object a, Object b)

– 比较两个对象的。
– 底层进行非空判断,从而可以避免空指针异常。更安全!!推荐使用!!

```java
   public static boolean equals(Object a, Object b) {
                     return a == b || a != null && a.equals(b);
             }
```

2.public static boolean isNull(Object obj)

– 判断变量是否为null ,为null返回true ,反之!

11.4 Date日期类的使用

  **构造器:**
        -- public Date():创建当前系统的此刻日期时间对象。
        -- public Date(long time):
    **方法:**
         -- public long getTime():返回自 1970 年 1 月 1 日 00:00:00 GMT 以来走过的总的毫秒数。
long endTime = new Date().getTime();
Date date = new Date(endTime);
System.out.println(date);

11.5 SimpleDateFormat

Date date = new Date();
System.out.println(date);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss EEE a");
String rs = simpleDateFormat.format(date);
System.out.println(rs);
// 1.问121s后的时间是多少。格式化输出。
// a. 得到当前时间对象
Date date = new Date();

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

long now = date.getTime();

long last = now + (121L * 1000);

String format = simpleDateFormat.format(last);

System.out.println(format);

11. DateFormate

11. Calendar日历类

Calendar代表了系统此刻日期对应的日历对象。
Calendar是一个抽象类,不能直接创建对象。
Calendar日历类创建日历对象的语法:

    ```java
     Calendar rightNow = Calendar.getInstance();
    ```

​ Calendar的方法:
​ 1.public static Calendar getInstance(): 返回一个日历类的对象。
​ 2.public int get(int field):取日期中的某个字段信息。
​ 3.public void set(int field,int value):修改日历的某个字段信息。
​ 4.public void add(int field,int amount):为某个字段增加/减少指定的值
​ 5.public final Date getTime(): 拿到此刻日期对象。
​ 6.public long getTimeInMillis(): 拿到此刻时间毫秒值

十二、Math

Math用于做数学运算。
Math类中的方法全部是静态方法,直接用类名调用即可。

方法:
方法名 说明
​ public static int abs(int a) 获取参数a的绝对值:
​ public static double ceil(double a) 向上取整
​ public static double floor(double a) 向下取整
​ public static double pow(double a, double b) 获取a的b次幂
​ public static long round(double a) 四舍五入取整

十三、System

目标:System系统类的使用。

​ System代表当前系统。
静态方法:
​ 1.public static void exit(int status):终止JVM虚拟机,非0是异常终止。
​ 2.public static long currentTimeMillis():获取当前系统此刻时间毫秒值。
​ 3.可以做数组的拷贝。
​ arraycopy(Object var0, int var1, Object var2, int var3, int var4);

  • 参数一:原数组
  • 参数二:从原数组的哪个位置开始赋值。
  • 参数三:目标数组
  • 参数四:赋值到目标数组的哪个位置
  • 参数五:赋值几个。

十四、BigDecimal

BigDecimal是通过大数据一系列处理进行计算

​ public BigDecimal add(BigDecimal value) 加法运算

​ public BigDecimal subtract(BigDecimal value) 减法运算

​ public BigDecimal multiply(BigDecimal value) 乘法运算

​ public BigDecimal divide(BigDecimal value) 除法运算

​ public double doubleValue():把BigDecimal转换成double类型。

BigDecimal a1 = BigDecimal.valueOf(a);
BigDecimal b1 = BigDecimal.valueOf(b);

BigDecimal c1 = a1.add(b1);
BigDecimal c2 = a1.divide(b1);
System.out.println(c1);
System.out.println(c2);

十五、包装类

概述

在Java中认为一切皆是对象,引用数据类型就是对象了。

Java为了满足一切皆对象,所以将8种基本数据类型加入了包装类

15.1 八种数据类型

基本数据类型包装类
byteByte
shortShort
intInteger(特殊)
longLong
floatFloat
doubleDouble
charCharacter(特殊)
booleanBoolean

自动装箱:可以直接把基本数据类型的值或者变量赋值给包装类。
自动拆箱:可以把包装类的变量直接赋值给基本数据类型。

15.2 用处及特殊功能

具体来看特殊功能主要有:

1.可以把基本数据类型的值转换成字符串类型的值。(没啥用)
    -- 调用toString()方法。
    -- 调用Integer.toString(基本数据类型的值)得到字符串。
    -- 直接把基本数据类型+空字符串就得到了字符串。
2.把字符串类型的数值转换成对应的基本数据类型的值。(真的很有用)
    -- Xxx.parseXxx("字符串类型的数值")
    -- Xxx.valueOf("字符串类型的数值"):推荐使用!
double b = 0.1;
double c = 99.9;
System.out.println(b + c);

String dStr = "0.1";
String eStr = "99";
double d = Double.valueOf(dStr);
Integer e = Integer.valueOf(eStr);
System.out.println(d);
System.out.println(d + c);
System.out.println(e);

十六、正则表达式

概述

正则表达式很适合做校验,代码简单,优雅!

16.1 常用正则表达式

  字符类
         [abc] a、b 或 c(简单类)
         [^abc] 任何字符,除了 a、b 或 c(否定)
         [a-zA-Z] a 到 z 或 A 到 Z,两头的字母包括在内(范围)
         [a-d[m-p]] a 到 d 或 m 到 p:[a-dm-p](并集)
         [a-z&&[def23]] d、e 或 f(交集)
         [a-z&&[^bc]] a 到 z,除了 b 和 c:[ad-z](减去)
         [a-z&&[^m-p]] a 到 z,而非 m 到 p:[a-lq-z](减去)
 
 预定义字符类
     . 任何字符
     \d 数字:[0-9]
     \D 非数字: [^0-9]
     \s 空白字符:[ \t\n\x0B\f\r]
     \S 非空白字符:[^\s]
     \w 单词字符:[a-zA-Z_0-9]
     \W 非单词字符:[^\w]

以上正则匹配只能校验单个字符。

Greedy 数量词
     X? X,一次或一次也没有
     X* X,零次或多次
     X+ X,一次或多次
     X{n} X,恰好 n 次
     X{n,} X,至少 n 次
     X{n,m} X,至少 n 次,但是不超过 m 次

//public boolean matches(String regex):判断是否与正则表达式匹配,匹配返回true

16.2 正则表达式的应用

目标:正则表达式在方法中的应用。
public String[] split(String regex):
– 按照正则表达式匹配的内容进行分割字符串,反回一个字符串数组。
public String replaceAll(String regex,String newStr)
– 按照正则表达式匹配的内容进行替换

16.3 正则表达式爬取数据

String rs = "来黑马程序学习Java,电话020-43422424,或者联系邮箱" +
             "itcast@itcast.cn,电话18762832633,0203232323" +
             "邮箱bozai@itcast.cn,400-100-3233 ,4001003232";
        // 需求:从上面的内容中爬取出 电话号码和邮箱。
        // 1.定义爬取规则
String regex = "(\\w{1,}@\\w{2,10}(\\.\\w{2,10}){1,2})|(1[3-9]\\d{9})|(0\\d{2,5}-?\\d{5,15})|400-?\\d{3,8}-?\\d{3,8}";
// 2.编译正则表达式成为一个匹配规则对象
Pattern pattern = Pattern.compile(regex);
// 3.通过匹配规则对象得到一个匹配数据内容的匹配器对象
Matcher matcher = pattern.matcher(rs);
// 4.通过匹配器去内容中爬取出信息
while(matcher.find()){
    System.out.println(matcher.group());
}

十七、泛型(难点)

概念

泛型就是一个标签:<数据类型>

泛型可以在编译阶段约束只能操作某种数据的类型

注意

泛型和集合只能支持引用数据类型,不支持基本数据类型

17.1 泛型概述

在前面学习集合时,我们都知道集合中是可以存放任意对象的,只要把对象存储集合后,那么这时他们都会被提升成Object类型。当我们在取出每一个对象,并且进行相应的操作,这时必须采用类型转换。

大家观察下面代码:

public class GenericDemo {
	public static void main(String[] args) {
		Collection coll = new ArrayList();
		coll.add("abc");
		coll.add("itcast");
		coll.add(5);//由于集合没有做任何限定,任何类型都可以给其中存放
		Iterator it = coll.iterator();
		while(it.hasNext()){
			//需要打印每个字符串的长度,就要把迭代出来的对象转成String类型
			String str = (String) it.next();
			System.out.println(str.length());
		}
	}
}

程序在运行时发生了问题java.lang.ClassCastException。 为什么会发生类型转换异常呢? 我们来分析下:由于集合中什么类型的元素都可以存储。导致取出时强转引发运行时 ClassCastException。 怎么来解决这个问题呢? Collection虽然可以存储各种对象,但实际上通常Collection只存储同一类型对象。例如都是存储字符串对象。因此在JDK5之后,新增了泛型(Generic)语法,让你在设计API时可以指定类或方法支持泛型,这样我们使用API的时候也变得更为简洁,并得到了编译时期的语法检查。

  • 泛型:可以在类或方法中预支地使用未知的类型。

tips:一般在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类型为Object类型。

17.2 使用泛型的好处

上一节只是讲解了泛型的引入,那么泛型带来了哪些好处呢?

  • 将运行时期的ClassCastException,转移到了编译时期变成了编译失败。
  • 避免了类型强转的麻烦。

通过我们如下代码体验一下:

public class GenericDemo2 {
	public static void main(String[] args) {
        Collection<String> list = new ArrayList<String>();
        list.add("abc");
        list.add("itcast");
        // list.add(5);//当集合明确类型后,存放类型不一致就会编译报错
        // 集合已经明确具体存放的元素类型,那么在使用迭代器的时候,迭代器也同样会知道具体遍历元素类型
        Iterator<String> it = list.iterator();
        while(it.hasNext()){
            String str = it.next();
            //当使用Iterator<String>控制元素类型后,就不需要强转了。获取到的元素直接就是String类型
            System.out.println(str.length());
        }
	}
}

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

17.3 泛型的定义与使用

我们在集合中会大量使用到泛型,这里来完整地学习泛型知识。

泛型,用来灵活地将数据类型应用到不同的类、方法、接口当中。将数据类型作为参数进行传递。

定义和使用含有泛型的类

定义格式:

修饰符 class 类名<代表泛型的变量> {  }

例如,API中的ArrayList集合:

泛型在定义的时候不具体,使用的时候才变得具体。在使用的时候确定泛型的具体数据类型。

class ArrayList<E>{ 
    public boolean add(E e){ }

    public E get(int index){ }
   	....
}

使用泛型: 即什么时候确定泛型。

在创建对象的时候确定泛型

例如,ArrayList<String> list = new ArrayList<String>();

此时,变量E的值就是String类型,那么我们的类型就可以理解为:

class ArrayList<String>{ 
     public boolean add(String e){ }

     public String get(int index){  }
     ...
}

再例如,ArrayList<Integer> list = new ArrayList<Integer>();

此时,变量E的值就是Integer类型,那么我们的类型就可以理解为:

class ArrayList<Integer> { 
     public boolean add(Integer e) { }

     public Integer get(int index) {  }
     ...
}

含有泛型的方法

定义格式:

修饰符 <代表泛型的变量> 返回值类型 方法名(参数){  }

例如,

public class MyGenericMethod {	  
    public <MVP> void show(MVP mvp) {
    	System.out.println(mvp.getClass());
    }
    
    public <MVP> MVP show2(MVP mvp) {	
    	return mvp;
    }
}

调用方法时,确定泛型的类型

public class GenericMethodDemo {
    public static void main(String[] args) {
        // 创建对象
        MyGenericMethod mm = new MyGenericMethod();
        // 演示看方法提示
        mm.show("aaa");
        mm.show(123);
        mm.show(12.45);
    }
}

含有泛型的接口

定义格式:

修饰符 interface接口名<代表泛型的变量> {  }

例如,

public interface MyGenericInterface<E>{
	public abstract void add(E e);
	
	public abstract E getE();  
}

使用格式:

1、定义类时确定泛型的类型

例如

public class MyImp1 implements MyGenericInterface<String> {
	@Override
    public void add(String e) {
        // 省略...
    }

	@Override
	public String getE() {
		return null;
	}
}

此时,泛型E的值就是String类型。

2、始终不确定泛型的类型,直到创建对象时,确定泛型的类型

例如

public class MyImp2<E> implements MyGenericInterface<E> {
	@Override
	public void add(E e) {
       	 // 省略...
	}

	@Override
	public E getE() {
		return null;
	}
}

确定泛型:

/*
 * 使用
 */
public class GenericInterface {
    public static void main(String[] args) {
        MyImp2<String>  my = new MyImp2<String>();  
        my.add("aa");
    }
}

17.4 泛型通配符

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

通配符基本使用

泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。

此时只能接受数据,不能往该集合中存储数据。

举个例子大家理解使用即可:

public static void main(String[] args) {
    Collection<Intger> list1 = new ArrayList<Integer>();
    getElement(list1);
    Collection<String> list2 = new ArrayList<String>();
    getElement(list2);
}
public static void getElement(Collection<?> coll){}
// ?代表可以接收任意类型
泛型不存在继承关系 Collection<Object> list = new ArrayList<String>();这种是错误的

通配符高级使用

之前设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在JAVA的泛型中可以指定一个泛型的上限下限

泛型的上限

  • 格式类型名称 <? extends 类 > 对象名称
  • 意义只能接收该类型及其子类

泛型的下限

  • 格式类型名称 <? super 类 > 对象名称
  • 意义只能接收该类型及其父类型

比如:现已知Object类,String 类,Number类,Integer类,其中Number是Integer的父类

public static void main(String[] args) {
    Collection<Integer> list1 = new ArrayList<Integer>();
    Collection<String> list2 = new ArrayList<String>();
    Collection<Number> list3 = new ArrayList<Number>();
    Collection<Object> list4 = new ArrayList<Object>();
    
    getElement(list1);
    getElement(list2);//报错
    getElement(list3);
    getElement(list4);//报错
  
    getElement2(list1);//报错
    getElement2(list2);//报错
    getElement2(list3);
    getElement2(list4);
  
}
// 泛型的上限:此时的泛型?,必须是Number类型或者Number类型的子类
public static void getElement1(Collection<? extends Number> coll){}
// 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
public static void getElement2(Collection<? super Number> coll){}

十八、Collection集合

18.1 集合概述

集合:集合是java中的一个容器,可以用来存储多个数据

区别

  • 数组的长度是固定的,集合的长度是可变的
  • 数组中存储的是同一类元素,可以存储任意类型数据,。集合存储是引用数据类型。

18.2 Set和List

  • Set 接口
    • HashSet 实现类
      • LinkedHashSet<> 实现类
    • TreeSer<> 实现类
  • List 接口
    • ArryList 实现类
    • LinkList<> 实现类

18.3 集合特点

Set集合系列:添加的元素无序,不重复,无索引

​ — HastSet :添加的元素无序,不重复,无索引

​ —LinkedHashSet: 添加的元素有序,不重复,无索引

​ — TreeSet: 不重复,无索引,升序排序

List集合: 添加的元素有序,可重复,有索引

​ — ArrayList: 添加的元素有序,可重复,有索引

​ — LinekdList: 添加的元素有序,可重复,有索引

18.4 常用的api

Collection API如下:
- public boolean add(E e): 把给定的对象添加到当前集合中 。
- public void clear() :清空集合中所有的元素。
- public boolean remove(E e): 把给定的对象在当前集合中删除。
- public boolean contains(Object obj): 判断当前集合中是否包含给定的对象。
- public boolean isEmpty(): 判断当前集合是否为空。
- public int size(): 返回集合中元素的个数。
- public Object[] toArray(): 把集合中的元素,存储到数组中

// HashSet:添加的元素是无序,不重复,无索引。
Collection<String> sets = new HashSet<>();
// 1.添加元素,添加成功返回true.
System.out.println(sets.add("贾乃亮")); // true
System.out.println(sets.add("贾乃亮")); // false
System.out.println(sets.add("王宝强")); // true
sets.add("陈羽凡");
System.out.println(sets); // 集合重写了toString()方法,默认打印出内容信息
// 2.清空集合的元素。
//sets.clear();
//System.out.println(sets);

// 3.判断集合是否为空 是空返回true 反之
System.out.println(sets.isEmpty()); // false

// 4.获取集合的大小
System.out.println(sets.size()); // 3

// 5.判断集合中是否包含某个元素 。
System.out.println(sets.contains("贾乃亮"));

// 6.删除某个元素:如果有多个重复元素默认删除前面的第一个!
sets.remove("陈羽凡");
System.out.println(sets);

// 7.把集合转换成数组
Object[] arrs = sets.toArray();
System.out.println("数组:"+ Arrays.toString(arrs));

String[] arrs1 = sets.toArray(String[]::new); // 以后再了解,指定转换的数组类型!
System.out.println("数组:"+ Arrays.toString(arrs1));

System.out.println("---------------------拓展---------------------------");
Collection<String> c1 = new ArrayList<>();
c1.add("李小璐");
c1.add("马蓉");

Collection<String> c2 = new ArrayList<>();
c2.add("白百合");

c1.addAll(c2); // 把c2集合的元素全部倒入到c1
System.out.println(c1);

18.5 遍历

概念

遍历就是一个一个把容器中的元素访问一遍

遍历的三种形式

  1. 迭代器
Collection<String> objects = new ArrayList<>();

objects.add("魏聪");
objects.add("陈采慧");
objects.add("唐树泽");
objects.add("魏聪");

System.out.println(objects);

Iterator<String> it = objects.iterator();

/* System.out.println(it.next());
    System.out.println(it.next());
    System.out.println(it.next());
    System.out.println(it.next());*/

while (it.hasNext()) {
    String ele = it.next();
    System.out.println(ele);
}
  1. foreach(增强的for循环)

    Collection<String> objects = new ArrayList<>();
    
    objects.add("魏聪");
    objects.add("陈采慧");
    objects.add("唐树泽");
    objects.add("魏聪");
    
    System.out.println(objects);
    
    for (String object : objects) {
        System.out.println(object);
    }
    
  2. Lambda技术表达式

    Collection<String> objects = new ArrayList<>();
    
    objects.add("魏聪");
    objects.add("陈采慧");
    objects.add("唐树泽");
    objects.add("魏聪");
    
    objects.forEach(s -> {
        System.out.println(s);
    });
    
    objects.forEach(s ->System.out.println(s));
    
    objects.forEach(System.out::println);
    

十九、数据结构

19.1 数据结构介绍

数据结构 : 数据用什么样的方式组合在一起。

19.2 常见数据结构

数据存储的常用结构有:栈、队列、数组、链表和红黑树。我们分别来了解一下:

  • stack,又称堆栈,它是运算受限的线性表,其限制是仅允许在标的一端进行插入和删除操作,不允许在其他任何位置进行添加、查找、删除等操作。

简单的说:采用该结构的集合,对元素的存取有如下的特点

  • 先进后出(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素)。例如,子弹压进弹夹,先压进去的子弹在下面,后压进去的子弹在上面,当开枪时,先弹出上面的子弹,然后才能弹出下面的子弹。
  • 栈的入口、出口的都是栈的顶端位置。

这里两个名词需要注意:

  • 压栈:就是存元素。即,把元素存储到栈的顶端位置,栈中已有元素依次向栈底方向移动一个位置。
  • 弹栈:就是取元素。即,把栈的顶端位置元素取出,栈中已有元素依次向栈顶方向移动一个位置。
队列
  • 队列queue,简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。

简单的说,采用该结构的集合,对元素的存取有如下的特点:

  • 先进先出(即,存进去的元素,要在后它前面的元素依次取出后,才能取出该元素)。例如,小火车过山洞,车头先进去,车尾后进去;车头先出来,车尾后出来。
  • 队列的入口、出口各占一侧。例如,下图中的左侧为入口,右侧为出口。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cxBEtHsZ-1640579319687)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\队列.png)]

数组
  • 数组:Array,是有序的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素。就像是一排出租屋,有100个房间,从001到100每个房间都有固定编号,通过编号就可以快速找到租房子的人。

简单的说,采用该结构的集合,对元素的存取有如下的特点:

  • 查找元素快:通过索引,可以快速访问指定位置的元素

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7I655Nuo-1640579319688)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\数组查询快.png)]

  • 增删元素慢

  • 指定索引位置增加元素:需要创建一个新数组,将指定新元素存储在指定索引位置,再把原数组元素根据索引,复制到新数组对应索引的位置。如下图[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gtja67ot-1640579319693)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\数组添加.png)]

  • **指定索引位置删除元素:**需要创建一个新数组,把原数组元素根据索引,复制到新数组对应索引的位置,原数组中指定索引位置元素不复制到新数组中。如下图[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f2U304C4-1640579319694)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\数组删除.png)]

链表
  • 链表:linked list,由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时i动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。我们常说的链表结构有单向链表与双向链表,那么这里给大家介绍的是单向链表

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3dWzFmZU-1640579319695)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\单链表结构特点.png)]

简单的说,采用该结构的集合,对元素的存取有如下的特点:

  • 多个结点之间,通过地址进行连接。例如,多个人手拉手,每个人使用自己的右手拉住下个人的左手,依次类推,这样多个人就连在一起了。

  • 查找元素慢:想查找某个元素,需要通过连接的节点,依次向后查找指定元素

  • 增删元素快:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SuQQqcC5-1640579319695)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\链表.png)]树数据结构

    树是有很多节点组成的

19.3. 树基本结构介绍

树具有的特点:

  1. 每一个节点有零个或者多个子节点
  2. 没有父节点的节点称之为根节点,一个树最多有一个根节点。
  3. 每一个非根节点有且只有一个父节点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6wmSTIwP-1640579319696)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1562637870270.png)]

名词含义
节点指树中的一个元素
节点的度节点拥有的子树的个数,二叉树的度不大于2
叶子节点度为0的节点,也称之为终端结点
高度叶子结点的高度为1,叶子结点的父节点高度为2,以此类推,根节点的高度最高
根节点在第一层,以此类推
父节点若一个节点含有子节点,则这个节点称之为其子节点的父节点
子节点子节点是父节点的下一层节点
兄弟节点拥有共同父节点的节点互称为兄弟节点

二叉树

如果树中的每个节点的子节点的个数不超过2,那么该树就是一个二叉树。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-doCfOYkb-1640579319696)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1565609702432.png)]

二叉查找树/二叉排序树

二叉查找树的特点:

  1. 左子树上所有的节点的值均小于等于他的根节点的值
  2. 右子树上所有的节点值均大于或者等于他的根节点的值
  3. 每一个子节点最多有两个子树

案例演示(20,18,23,22,17,24,19)数据的存储过程;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NyuIETH1-1640579319697)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1565611710800.png)]

增删改查的性能都很高!!!

遍历获取元素的时候可以按照"左中右"的顺序进行遍历;

注意:二叉查找树存在的问题:会出现"瘸子"的现象,影响查询效率。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fDVFT32z-1640579319697)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1565611927771.png)]

平衡二叉树

(基于查找二叉树,但是让树不要太高,尽量让树的元素均衡分布。这样综合性能就高了)

概述

为了避免出现"瘸子"的现象,减少树的高度,提高我们的搜素效率,又存在一种树的结构:“平衡二叉树”

规则:它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树

如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OIp66C7D-1640579319698)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1562642409744.png)]

如下图所示,左图是一棵平衡二叉树,根节点10,左右两子树的高度差是1,而右图,虽然根节点左右两子树高度差是0,但是右子树15的左右子树高度差为2,不符合定义,

所以右图不是一棵平衡二叉树。

旋转

在构建一棵平衡二叉树的过程中,当有新的节点要插入时,检查是否因插入后而破坏了树的平衡,如果是,则需要做旋转去改变树的结构。

左旋:

左旋就是将节点的右支往左拉,右子节点变成父节点,并把晋升之后多余的左子节点出让给降级节点的右子节点;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lg7imy6l-1640579319698)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\左旋.png)]

右旋:

将节点的左支往右拉,左子节点变成了父节点,并把晋升之后多余的右子节点出让给降级节点的左子节点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NgqhNIaF-1640579319699)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\右旋.png)]

举个例子,像上图是否平衡二叉树的图里面,左图在没插入前"19"节点前,该树还是平衡二叉树,但是在插入"19"后,导致了"15"的左右子树失去了"平衡",

所以此时可以将"15"节点进行左旋,让"15"自身把节点出让给"17"作为"17"的左树,使得"17"节点左右子树平衡,而"15"节点没有子树,左右也平衡了。如下图,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FTopjz0N-1640579319699)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1562644020804.png)]

由于在构建平衡二叉树的时候,当有新节点插入时,都会判断插入后时候平衡,这说明了插入新节点前,都是平衡的,也即高度差绝对值不会超过1。当新节点插入后,

有可能会有导致树不平衡,这时候就需要进行调整,而可能出现的情况就有4种,分别称作左左,左右,右左,右右

左左

左左即为在原来平衡的二叉树上,在节点的左子树的左子树下,有新节点插入,导致节点的左右子树的高度差为2,如下即为"10"节点的左子树"7",的左子树"4",插入了节点"5"或"3"导致失衡。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zeSPbc9f-1640579319699)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1562644117681.png)]

左左调整其实比较简单,只需要对节点进行右旋即可,如下图,对节点"10"进行右旋,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aZPJJxIV-1640579319700)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1562645661857.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oSMgBruU-1640579319700)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1562645709201.png)]

左右

左右即为在原来平衡的二叉树上,在节点的左子树的右子树下,有新节点插入,导致节点的左右子树的高度差为2,如上即为"11"节点的左子树"7",的右子树"9",

插入了节点"10"或"8"导致失衡。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kp8hK5lk-1640579319700)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1562644916480.png)]

左右的调整就不能像左左一样,进行一次旋转就完成调整。我们不妨先试着让左右像左左一样对"11"节点进行右旋,结果图如下,右图的二叉树依然不平衡,而右图就是接下来要

讲的右左,即左右跟右左互为镜像,左左跟右右也互为镜像。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bpRTju9o-1640579319701)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1562645170454.png)]

左右这种情况,进行一次旋转是不能满足我们的条件的,正确的调整方式是,将左右进行第一次旋转,将左右先调整成左左,然后再对左左进行调整,从而使得二叉树平衡。

即先对上图的节点"7"进行左旋,使得二叉树变成了左左,之后再对"11"节点进行右旋,此时二叉树就调整完成,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LixrDOGC-1640579319701)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1562645351977.png)]

右左

右左即为在原来平衡的二叉树上,在节点的右子树的左子树下,有新节点插入,导致节点的左右子树的高度差为2,如上即为"11"节点的右子树"15",的左子树"13",

插入了节点"12"或"14"导致失衡。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z0CLFgMK-1640579319701)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1562645765291.png)]

前面也说了,右左跟左右其实互为镜像,所以调整过程就反过来,先对节点"15"进行右旋,使得二叉树变成右右,之后再对"11"节点进行左旋,此时二叉树就调整完成,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gZdKjBgN-1640579319702)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1562645894833.png)]

右右

右右即为在原来平衡的二叉树上,在节点的右子树的右子树下,有新节点插入,导致节点的左右子树的高度差为2,如下即为"11"节点的右子树"13",的左子树"15",插入了节点

"14"或"19"导致失衡。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DiiI395v-1640579319702)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1562645951703.png)]

右右只需对节点进行一次左旋即可调整平衡,如下图,对"11"节点进行左旋。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QyAXZ38K-1640579319702)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1562646135227.png)]

红黑树

就是平衡的二叉查找树!!

概述

红黑树是一种自平衡的二叉查找树,是计算机科学中用到的一种数据结构,它是在1972年由Rudolf Bayer发明的,当时被称之为平衡二叉B树,后来,在1978年被

Leoj.Guibas和Robert Sedgewick修改为如今的"红黑树"。它是一种特殊的二叉查找树,红黑树的每一个节点上都有存储位表示节点的颜色,可以是红或者黑;

红黑树不是高度平衡的,它的平衡是通过"红黑树的特性"进行实现的;

红黑树的特性:

  1. 每一个节点或是红色的,或者是黑色的。
  2. 根节点必须是黑色
  3. 每个叶节点(Nil)是黑色的;(如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点)
  4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况)
  5. 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点;

如下图所示就是一个

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IGsw3eNC-1640579319703)(F:\人生苦短,我用Java\java进阶13天资料\day05\resources\img\1562653205543.png)]

在进行元素插入的时候,和之前一样; 每一次插入完毕以后,使用黑色规则进行校验,如果不满足红黑规则,就需要通过变色,左旋和右旋来调整树,使其满足红黑规则;

二十、List,ArrayList,LinekdList

20.1 ArrayList

以多态的方式存储

List<String> list = new ArrayList<>();
list.add("java1");
list.add("java1");
list.add("java2");
list.add("java2");
System.out.println(list);

list.add(1,"mysql");
System.out.println(list);

list.remove(2);
System.out.println(list);

list.set(1,"mybatis");
System.out.println(list);

20.2 LinekdList

LinkedList除了拥有List集合的全部功能还多了很多操作首尾元素的特殊功能:
-public void addFirst(E e):将指定元素插入此列表的开头。
-public void addLast(E e):将指定元素添加到此列表的结尾。
-public E getFirst():返回此列表的第一个元素。
-public E getLast():返回此列表的最后一个元素。
-public E removeFirst():移除并返回此列表的第一个元素。
-public E removeLast():移除并返回此列表的最后一个元素。
-public E pop():从此列表所表示的堆栈处弹出一个元素。
-public void push(E e):将元素推入此列表所表示的堆栈。

用linkedlist做一个堆和队列

//做一个队列
LinkedList<String> list = new LinkedList<>();

list.addLast("莫寒");
list.addLast("莫寒1");
list.addLast("莫寒2");
list.addLast("莫寒3");
list.addLast("莫寒4");
System.out.println(list);

list.removeFirst();
list.removeFirst();
list.removeFirst();
list.removeFirst();
list.removeFirst();
System.out.println(list);


//做一个栈
LinkedList<String> list1 = new LinkedList<>();
list1.push("哈哈啊");
list1.push("哈哈啊1");
list1.push("哈哈啊2");
list1.push("哈哈啊3");
list1.push("哈哈啊4");
list1.push("哈哈啊5");
System.out.println(list1);

list1.pop();
System.out.println(list1);

二十一、Set

21.1 Set的基本使用

Set<String> list = new HashSet<>();
list.add("莫寒寒");
list.add("莫寒寒1");
list.add("莫寒寒2");
list.add("莫寒寒3");
list.add("莫寒寒4");
System.out.println(list);

21.2 set的判断特性

1.对于有值特性的,Set集合可以直接判断进行去重复。
2.对于引用数据类型的类对象,Set集合是按照如下流程进行是否重复的判断。
Set集合会让两两对象,先调用自己的hashCode()方法得到彼此的哈希值(所谓的内存地址)
然后比较两个对象的哈希值是否相同,如果不相同则直接认为两个对象不重复。
如果哈希值相同,会继续让两个对象进行equals比较内容是否相同,如果相同认为真的重复了
如果不相同认为不重复。

            Set集合会先让对象调用hashCode()方法获取两个对象的哈希值比较
               /                     \
            false                    true
            /                          \
        不重复                        继续让两个对象进行equals比较
                                       /          \
                                     false        true
                                      /             \
                                    不重复          重复了

需求:只要对象内容一样,就希望集合认为它们重复了。重写hashCode和equals方法。

小结
如果希望Set集合认为两个对象只要内容一样就重复了,必须重写对象的hashCode和equals方法。

21.3 HashSet的特性

JDK 1.8之前:哈希表 = 数组 + 链表 + (哈希算法)
JDK 1.8之后:哈希表 = 数组 + 链表 + 红黑树 + (哈希算法)
当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

Set系列集合是基于哈希表存储数据的
它的增删改查的性能都很好!!但是它是无序不重复的!如果不在意当然可以使用!

21.4 LinkedHashSet

​ 如果希望元素可以重复,又有索引,查询要快用ArrayList集合。(用的最多)
​ 如果希望元素可以重复,又有索引,增删要快要用LinkedList集合。(适合查询元素比较少的情况,经常要首尾操作元素的情况)
​ 如果希望增删改查都很快,但是元素不重复以及无序无索引,那么用HashSet集合。
​ 如果希望增删改查都很快且有序,但是元素不重复以及无索引,那么用LinkedHashSet集合。

21.5 TreeSet

TreeSet集合对自定义引用数据类型排序,默认无法进行。
但是有两种方式可以让程序员定义大小规则:
a.直接为对象的类实现比较器规则接口Comparable,重写比较方法(拓展方式)
b.直接为集合设置比较器Comparator对象,重写比较方法
注意:如果类和集合都带有比较规则,优先使用集合自带的比较规则。

匿名内部类的使用排序

 Set<Employee> employees1 = new TreeSet<>(new Comparator<Employee>() {
     @Override
     public int compare(Employee o1, Employee o2) {
         // o1比较者   o2被比较者
         // 如果程序员认为比较者大于被比较者 返回正数!
         // 如果程序员认为比较者小于被比较者 返回负数!
         // 如果程序员认为比较者等于被比较者 返回0!
         return o1.getAge() - o2.getAge();
     }
 });

二十二、Collections工具类使用

  1. 给集合批量添加元素

public static boolean addAll(Collection<? super T> c, T… elements)

 // 1. 给集合批量添加元素
Collection<String> names = new ArrayList<>();
Collections.addAll(names,"莫寒","ccf");
System.out.println(names);
  1. public static void shuffle(List<?> list) :打乱集合顺序。(只能打乱list)

     //2. 打乱集合顺序
    List<String> animals = new ArrayList<>();
    Collections.addAll(animals,"狗","猫","蛇");
    System.out.println(animals);
    Collections.shuffle(animals);
    System.out.println(animals);
    
  2. public static void sort(List list):将集合中元素按照默认规则排序。

     //3.排序
    Collections.sort(animals);
    System.out.println(animals);
    
  3. public static void sort(List list,Comparator<? super T> ):将集合中元素按照指定规则排序。

    @Override
    public int compareTo(Object o) {
        Orange o2 = (Orange) o;
        if (this.weight > o2.weight) return 1;
        if (this.weight < o2.weight) return -1;
        return 0;
    }
    
    ArrayList<Orange> list2 = new ArrayList<>();
    Orange orange111 = new Orange("红橘子",125.5,"5.5元");
    Orange orange11 = new Orange("红橘子",55.5,"5.5元");
    Orange orange21 = new Orange("红橘子",65.5,"5.5元");
    Orange orange31 = new Orange("红橘子",75.5,"5.5元");
    Collections.addAll(list2,orange,orange1,orange2,orange3);
    Collections.sort(list2, new Comparator<Orange>() {
        @Override
        public int compare(Orange o1, Orange o2) {
            if (o1.getWeight() > o2.getWeight()) return -1;
            if (o1.getWeight() < o2.getWeight()) return 1;
    
            return 0;
        }
    });
    

二十三、 可变参数

public static void main(String[] args) {
    getArray();
    getArray(10);
    getArray(10,20,25);
    getNums(10,10,20,40,50)
}

public static void getArray(int...arr){
    System.out.println(arr.length);
    System.out.println(Arrays.toString(arr));
    System.out.println("------------");
}

public static void getNums(int age,int...nums){
    System.out.println(nums.length);
    System.out.println(Arrays.toString(nums));
    System.out.println("------------");
}

二十四、Map集合

概述

Map集合是Collection的另一个集合体系

  1. Map集合是一种双列结合,每个元素包含两个值
  2. Map以键值对存储元素
  3. Map集合也被称为:“键值对集合”
Map<String,Integer> map = new HashMap<>();
map.put("莫寒",18);
map.put("ccf",16);
map.put("莫寒1",180);
map.put("唐树泽",180);
map.put("王先康",1);
System.out.println(map);

24.1 Map的Api

  1. 添加元素:无序,不重复,无索引 put

    Map<String,Integer> map = new HashMap<>();
    map.put("莫寒",18);
    map.put("ccf",16);
    map.put("莫寒1",180);
    map.put("唐树泽",180);
    map.put("王先康",1);
    System.out.println(map);
    
  2. 清空集合

    map.clear();
    System.out.println(map);
    
  3. 判断集合是否为空,为空则返回true

    System.out.println(map.isEmpty());;
    
  4. 通过键获取相应值

    System.out.println(map.get("ccf"));
    
  5. 根据键删除对应值

    System.out.println(map.remove("王先康"));;
    System.out.println(map);
    
  6. 判断是否包含某个键,包含返回true

    System.out.println(map.containsKey("ccf"));
    System.out.println(map.containsKey("10"));
    
  7. 判断是否包含某个值,包含返回true

    System.out.println(maps.containsValue(1000)); // true
    System.out.println(maps.containsValue(10)); // true
    System.out.println(maps.containsValue("30")); // false 包含的是整数30不是字符串。
    
  8. 获取全部键的集合:public Set keySet()

    // Map集合的键是无序不重复的,所以返回的是一个Set集合。
    Set<String> keys = maps.keySet();
    for (String key : keys) {
        System.out.println(key);
    }
    
  9. .获取全部值的集合:Collection values();

    // Map集合的值是不做要求的,可能重复,所以值要用Collection集合接收!
    Collection<Integer> values = maps.values();
    for (Integer value : values) {
        System.out.println(value);
    }
    
  10. 集合的大小

    System.out.println(maps.size());
    
  11. 合并其他Map集合。(拓展)

    Map<String,Integer> maps2 = new HashMap<>();
    maps2.put("xiaoMi" , 1);
    maps2.put("🔨手机" , 10);
    maps2.put("手表" , 10000);
    maps.putAll(maps2); // 把Map集合maps2的数据全部倒入到maps集合中去
    System.out.println(maps);
    

24.2 Map的遍历

思想

先转换成Set,再遍历

  1. 先转换成Set再遍历
Map<String , Integer> maps = new HashMap<>();
// 1.添加元素: 无序,不重复,无索引。
maps.put("娃娃",30);
maps.put("iphoneX",100);//  Map集合后面重复的键对应的元素会覆盖前面重复的整个元素!
maps.put("huawei",1000);
maps.put("生活用品",10);
maps.put("手表",10);
System.out.println(maps);
// maps = {huawei=1000, 手表=10, 生活用品=10, iphoneX=100, 娃娃=30}

Set<String> keySet = maps.keySet();

System.out.println(keySet);

for (String s : keySet) {
    Integer integer = maps.get(s);
    System.out.println(s + "=" + integer);
}
  1. 把map每个换成一个实体类

    Map<String , Integer> maps = new HashMap<>();
    // 1.添加元素: 无序,不重复,无索引。
    maps.put("娃娃",30);
    maps.put("iphoneX",100);//  Map集合后面重复的键对应的元素会覆盖前面重复的整个元素!
    maps.put("huawei",1000);
    maps.put("生活用品",10);
    maps.put("手表",10);
    System.out.println(maps);
    
    Set<Map.Entry<String,Integer>> entries = maps.entrySet();
    
    for (Map.Entry<String, Integer> entry : entries) {
        String key = entry.getKey();
        Integer value = entry.getValue();
        System.out.println(key + "=>" + value);
    }
    
  2. 用lambda函数foreach jdk1.8之后

     Map<String , Integer> maps = new HashMap<>();
    // 1.添加元素: 无序,不重复,无索引。
    maps.put("娃娃",30);
    maps.put("iphoneX",100);//  Map集合后面重复的键对应的元素会覆盖前面重复的整个元素!
    maps.put("huawei",1000);
    maps.put("生活用品",10);
    maps.put("手表",10);
    System.out.println(maps);
    
    maps.forEach((k,v)->{
        System.out.println(k + "=" + v);
    });
    

24.3 HashMap存储自定义类型

练习:每位学生(姓名,年龄)都有自己的家庭住址。那么,既然有对应关系,则将学生对象和家庭住址存储到map集合中。学生作为键, 家庭住址作为值。

注意,学生姓名相同并且年龄相同视为同一名学生。

编写学生类:

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

    //构造方法
    //get/set
    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

编写测试类:

public class HashMapTest {
    public static void main(String[] args) {
        //1,创建Hashmap集合对象。
        Map<Student,String> map = new HashMap<Student,String>();
        //2,添加元素。
        map.put(new Student("lisi",28), "上海");
        map.put(new Student("wangwu",22), "北京");
        map.put(new Student("wangwu",22), "南京");
        
        //3,取出元素。键找值方式
        Set<Student> keySet = map.keySet();
        for(Student key: keySet){
            String value = map.get(key);
            System.out.println(key.toString()+"....."+value);
        }
    }
}
  • 当给HashMap中存放自定义对象时,如果自定义对象作为key存在,这时要保证对象唯一,必须复写对象的hashCode和equals方法(如果忘记,请回顾HashSet存放自定义对象)。
  • 如果要保证map中存放的key和取出的顺序一致,可以使用java.util.LinkedHashMap集合来存放。

24.4 LinkedHashMap介绍

我们知道HashMap保证成对元素唯一,并且查询速度很快,可是成对元素存放进去是没有顺序的,那么我们要保证有序,还要速度快怎么办呢?

在HashMap下面有一个子类LinkedHashMap,它是链表和哈希表组合的一个数据存储结构。

public class LinkedHashMapDemo {
    public static void main(String[] args) {
        LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
        map.put("邓超", "孙俪");
        map.put("李晨", "范冰冰");
        map.put("刘德华", "朱丽倩");
        Set<Entry<String, String>> entrySet = map.entrySet();
        for (Entry<String, String> entry : entrySet) {
            System.out.println(entry.getKey() + "  " + entry.getValue());
        }
    }
}

结果:

邓超  孙俪
李晨  范冰冰
刘德华  朱丽倩

24.5 TreeMap集合

1.TreeMap介绍

TreeMap集合和Map相比没有特有的功能,底层的数据结构是红黑树;可以对元素的**进行排序,排序方式有两种:自然排序比较器排序;到时使用的是哪种排序,取决于我们在创建对象的时候所使用的构造方法;

public TreeMap()									使用自然排序
public TreeMap(Comparator<? super K> comparator) 	比较器排

2.演示

案例演示自然排序

public static void main(String[] args) {
 	TreeMap<Integer, String> map = new TreeMap<Integer, String>();
  	map.put(1,"张三");
  	map.put(4,"赵六");
  	map.put(3,"王五");
  	map.put(6,"酒八");
  	map.put(5,"老七");
  	map.put(2,"李四");
  	System.out.println(map);
}

控制台的输出结果为:
{1=张三, 2=李四, 3=王五, 4=赵六, 5=老七, 6=酒八}

案例演示比较器排序

需求:

  1. 创建一个TreeMap集合,键是学生对象(Student),值是居住地 (String)。存储多个元素,并遍历。
  2. 要求按照学生的年龄进行升序排序,如果年龄相同,比较姓名的首字母升序, 如果年龄和姓名都是相同,认为是同一个元素;

实现:

为了保证age和name相同的对象是同一个,Student类必须重写hashCode和equals方法

public class Student {
    private int age;
    private String name;
	//省略get/set..
    public Student() {}
    public Student(int age, String name) {
        this.age = age;
        this.name = name;
    }
    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age &&
                Objects.equals(name, student.name);
    }
    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }
}
public static void main(String[] args) {
  	TreeMap<Student, String> map = new TreeMap<Student, String>(new Comparator<Student>() {
    	@Override
    	public int compare(Student o1, Student o2) {
      		//先按照年龄升序
      		int result = o1.getAge() - o2.getAge();
      		if (result == 0) {
        		//年龄相同,则按照名字的首字母升序
        		return o1.getName().charAt(0) - o2.getName().charAt(0);
      		} else {
        		//年龄不同,直接返回结果
        		return result;
      		}
    	}
  	});
  	map.put(new Student(30, "jack"), "深圳");
  	map.put(new Student(10, "rose"), "北京");
  	map.put(new Student(20, "tom"), "上海");
  	map.put(new Student(10, "marry"), "南京");
  	map.put(new Student(30, "lucy"), "广州");
  	System.out.println(map);
}
控制台的输出结果为:
{
  Student{age=10, name='marry'}=南京, 
  Student{age=10, name='rose'}=北京, 
  Student{age=20, name='tom'}=上海, 
  Student{age=30, name='jack'}=深圳, 
  Student{age=30, name='lucy'}=广州
}

二十五、排序算法

25.1 冒泡排序算法

25.1.1 冒泡排序概述

  • 一种排序的方式,对要进行排序的数据中相邻的数据进行两两比较,将较大的数据放在后面,依次对所有的数据进行操作,直至所有数据按要求完成排序
  • 如果有n个数据进行排序,总共需要比较n-1次
  • 每一次比较完毕,下一次的比较就会少一个数据参与

25.1.2 冒泡排序代码实现

/*
    冒泡排序:
        一种排序的方式,对要进行排序的数据中相邻的数据进行两两比较,将较大的数据放在后面,
        依次对所有的数据进行操作,直至所有数据按要求完成排序
 */
public class ArrayDemo {
    public static void main(String[] args) {
        //定义一个数组
        int[] arr = {7, 6, 5, 4, 3};
        System.out.println("排序前:" + Arrays.toString(arr));

        // 这里减1,是控制每轮比较的次数
        for (int x = 0; x < arr.length - 1; x++) {
            // -1是为了避免索引越界,-x是为了调高比较效率
            for (int i = 0; i < arr.length - 1 - x; i++) {
                if (arr[i] > arr[i + 1]) {
                    int temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                }
            }
        }
        System.out.println("排序后:" + Arrays.toString(arr));
    }
}

25.2 选择排序

25.2.1 选择排序概述

  • 另外一种排序的方式,选中数组的某个元素,其后面的元素依次和选中的元素进行两两比较,将较大的数据放在后面,依次从前到后选中每个元素,直至所有数据按要求完成排序
  • 如果有n个数据进行排序,总共需要比较n-1次
  • 每一次比较完毕,下一次的比较就会少一个数据参与

25.2.2 选择排序代码实现

/*
    选择排序:
        另外一种排序的方式,选中数组的某个元素,其后面的元素依次和选中的元素进行两两比较,将较大的数据放在后面,依次从前到后选中每个元素,直至所有数据按要求完成排序
 */
public class ArrayDemo {
    public static void main(String[] args) {
        //定义一个数组
        int[] arr = {7, 6, 5, 4, 3};
        System.out.println("排序前:" + Arrays.toString(arr));
  		// 这里减1,是控制比较的轮数
        for (int x = 0; x < arr.length; x++) {
            // 从x+1开始,直到最后一个元素
            for (int i = x+1; i < arr.length; i++) {
                if (arr[x] > arr[i]) {
                    int temp = arr[x];
                    arr[x] = arr[i];
                    arr[i] = temp;
                }
            }
        }
        System.out.println("排序后:" + Arrays.toString(arr));
    }
}

25.3 二分查找

25.3.1 普通查找和二分查找

普通查找

原理:遍历数组,获取每一个元素,然后判断当前遍历的元素是否和要查找的元素相同,如果相同就返回该元素的索引。如果没有找到,就返回一个负数作为标识(一般是-1)

二分查找

原理: 每一次都去获取数组的中间索引所对应的元素,然后和要查找的元素进行比对,如果相同就返回索引;

如果不相同,就比较中间元素和要查找的元素的值;

如果中间元素的值大于要查找的元素,说明要查找的元素在左侧,那么就从左侧按照上述思想继续查询(忽略右侧数据);

如果中间元素的值小于要查找的元素,说明要查找的元素在右侧,那么就从右侧按照上述思想继续查询(忽略左侧数据);

二分查找对数组是有要求的,数组必须已经排好序

25.3.2 二分查找代码实现

	public static void main(String[] args) {
        int[] arr = {10, 14, 21, 38, 45, 47, 53, 81, 87, 99};
        int index = binarySerach(arr, 38);
        System.out.println(index);
	}
	/**
     * 二分查找方法
     * @param arr 查找的目标数组
     * @param number 查找的目标值
     * @return 找到的索引,如果没有找到返回-1
     */
    public static int binarySerach(int[] arr, int number) {
        int start = 0;
        int end = arr.length - 1;

        while (start <= end) {
            int mid = (start + end) / 2;
            if (number == arr[mid]) {
                return mid + 1;
            } else if (number < arr[mid]) {
                end = mid - 1;
            } else if (number > arr[mid]) {
                start = mid + 1;
            }
        }
        return -1;  //如果数组中有这个元素,则返回
    }

二十六、异常

26.1 概念

异常是程序在“编译”或者“执行”过程中可能出现的异常

异常是应该尽量提前避免的

异常无法绝对避免

异常一旦出现了,如果没有提前处理,程序就会退出,我们要研究异常,才能让代码更加健壮。

异常分成两类:编译时异常,运行时异常

  • 编译时异常:继承了exception,编译阶段就报错,必须处理程序才能进行
  • 运行时异常,继承了runtimeException类,编译阶段不报错,运行报错

26.2 运行时异常

常见如下:

  1. 数组索引越界异常: ArrayIndexOutOfBoundsException。

    //1. 数组索引越界异常
    int[] arr = {0,1,2};
    System.out.println(arr[2]);
    System.out.println(arr[3]);
    
  2. 空指针异常 : NullPointerException。直接输出没有问题。但是调用空指针的变量的功能就会报错!!

    String name = null;
    System.out.println(name);
    System.out.println(name.length());
    
  3. 类型转换异常:ClassCastException。

    Object a = "哈哈哈";
    Integer name = (Integer) a;
    System.out.println(name);
    
  4. 迭代器遍历没有此元素异常:NoSuchElementException。

  5. 数学操作异常:ArithmeticException。

    System.out.println(1/0);
    
  6. 数字转换异常: NumberFormatException。

    String num = "123aa";
    Integer it = Integer.valueOf(num);
    System.out.println(it + 1);
    

26.3 异常默认处理机制

(1)默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException。
(2)异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM虚拟机。
(3)虚拟机接收到异常对象后,先在控制台直接输出异常栈信息数据。
(4)直接从当前执行的异常点干掉当前程序。
(5)后续代码没有机会执行了,因为程序已经死亡。

异常一旦出现,会自动创建异常对象,最终抛出给虚拟机,虚拟机
只要收到异常,就直接输出异常信息,干掉程序!!

26.4 编译阶段异常处理

一直往上抛,直至死亡

public static void main(String[] args) throws ParseException {
  System.out.println("开始处理程序");

  parseDate("2000/03-27 05:45:45");

  System.out.println("结束处理程序");
}

public static void parseDate(String time) throws ParseException {
  SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  Date d = sdf.parse(time);

  System.out.println(d);
}

将所有的异常列举出来

Date d = null;
InputStream is = null;
try {
    d = sdf.parse(time);
    is = new FileInputStream("D:/meinv.jpg");
} catch (ParseException |FileNotFoundException e) {
    e.printStackTrace();
}

全局处理异常

Date d = null;
InputStream is = null;
try {
    is = new FileInputStream("D:/meinv.jpg");
    d = sdf.parse(time);

} catch (Exception e) {
    e.printStackTrace();
}finally {
    System.out.println(111);
}

往上抛统一处理

try {
    parseDate("2000-02-22 05:05:05");
} catch (Exception e) {
    e.printStackTrace();
}

System.out.println("程序结束");
}

public static void parseDate(String time) throws Exception {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    Date d = sdf.parse(time);

    InputStream is = new FileInputStream("D:/meinv.jpg");

    System.out.println(d);
}

26.5 finally

可以在代码执行完毕以后进行资源的释放操作。
什么是资源?资源都是实现了Closeable接口的,都自带close()关闭方法!!

try{
    int a = 10 / 2 ;
    return a ;
}catch (Exception e){
    e.printStackTrace();
    return -1;
}finally {
    System.out.println("=====finally被执行");
    return 111; // 不建议在finally中写return,会覆盖前面所有的return值!
}

26.6 自定义异常

自定义异常:
自定义编译时异常.
a.定义一个异常类继承Exception.
b.重写构造器。
c.在出现异常的地方用throw new 自定义对象抛出!
编译时异常是编译阶段就报错,提醒更加强烈,一定需要处理!!

​ 自定义运行时异常.
a.定义一个异常类继承RuntimeException.
​ b.重写构造器。
​ c.在出现异常的地方用throw new 自定义对象抛出!
​ 提醒不强烈,编译阶段不报错!!运行时才可能出现!!
​ 小结:
​ 自定义异常是程序员自己定义的异常
继承Exception/RuntimeException,重写构造器。
在出现异常的地方用throw new 自定义异常对象抛出!

自定义继承Execption

public class ItheimaAgeIllegalException extends Exception {
    public ItheimaAgeIllegalException() {
    }

    public ItheimaAgeIllegalException(String message) {
        super(message);
    }

    public ItheimaAgeIllegalException(String message, Throwable cause) {
        super(message, cause);
    }

    public ItheimaAgeIllegalException(Throwable cause) {
        super(cause);
    }

    public ItheimaAgeIllegalException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

二十七、多线程

27.1 概念

什么是进程?
程序是静止的,运行中的程序就是进程。
进程的三个特征:
1.动态性 : 进程是运行中的程序,要动态的占用内存,CPU和网络等资源。
2.独立性 : 进程与进程之间是相互独立的,彼此有自己的独立内存区域。
3.并发性 : 假如CPU是单核,同一个时刻其实内存中只有一个进程在被执行。
CPU会分时轮询切换依次为每个进程服务,因为切换的速度非常
快,给我们的感觉这些进程在同时执行,这就是并发性。

并行:同一个时刻同时有多个在执行。

什么是线程?
线程是属于进程的。一个进程可以包含多个线程,这就是多线程。
线程是进程中的一个独立执行单元。
线程创建开销相对于进程来说比较小。
线程也支持“并发性”。

线程的作用:
可以提高程序的效率,线程也支持并发性,可以有更多机会得到CPU。
多线程可以解决很多业务模型。
大型高并发技术的核心技术。
设计到多线程的开发可能都比较难理解。

27.2 多线程的初步使用

  1. 继承Thread类
  2. 重写执行的方法run()
  3. 创建新线程
  4. 使用start开始执行
public class TestThread {
  public static void main(String[] args) {
    Thread mt = new MyThread();
    mt.start();
    for (int i = 0; i < 100; i++) {
      System.out.println("main线程" + i);
    }
  }
}

class MyThread extends Thread{
  @Override
  public void run() {
    for (int i = 0; i < 100; i++) {
      System.out.println("子线程" + i);
    }
  }
}

27.3 线程使用注意事项

必须使用start()方法执行线程,不然会当成普通类执行

27.4 线程的Api使用

  1. public void setName(String name):给当前线程取名字。
  2. public void getName():获取当前线程的名字。
  3. public static Thread currentThread()
public class TestThreadApi {
  public static void main(String[] args) {
    Thread t1 = new MyThread();
    t1.setName("线程一");
    t1.start();

    Thread t2 = new MyThread();
    t2.setName("线程二");
    t2.start();

    for (int i = 0; i < 10; i++) {
      System.out.println(Thread.currentThread().getName() + "=" + i);
    }
  }
}

class MyThread extends Thread{
  @Override
  public void run() {
    for (int i = 0; i < 10; i++) {
      System.out.println(Thread.currentThread().getName() + "=" + i);
    }
  }
}

sleep

 for (int i = 0; i < 10; i++) {
      System.out.println(i);

      try {
        //项目经理让我把这段代码加上
        //如果用户交了钱,就让我去掉
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }

27.5 通过有参构造线程名

public class TestDemo {
  public static void main(String[] args) {
    Thread myThread = new MyThread("线程一");
    myThread.start();

    Thread myThread1 = new MyThread("线程二");
    myThread1.start();

  }

}

class MyThread extends Thread{
  public MyThread(String name) {
    super(name);
  }

  @Override
  public void run() {
    for (int i = 0; i < 10; i++) {
      System.out.println(Thread.currentThread().getName() + i);
    }
  }
}

27.6 实现Runable接口

多线程是很有用的,我们在进程中创建线程的方式有三种:
(1)直接定义一个类继承线程类Thread,重写run()方法,创建线程对象
调用线程对象的start()方法启动线程。
(2)定义一个线程任务类实现Runnable接口,重写run()方法,创建线程任务对象,把
线程任务对象包装成线程对象, 调用线程对象的start()方法启动线程。
(3)实现Callable接口(拓展)。

b.实现Runnable接口的方式。
– 1.创建一个线程任务类实现Runnable接口。
– 2.重写run()方法
– 3.创建一个线程任务对象。
– 4.把线程任务对象包装成线程对象
– 5.调用线程对象的start()方法启动线程。

Thread的构造器:
– public Thread(){}
– public Thread(String name){}
– public Thread(Runnable target){}
– public Thread(Runnable target,String name){}

– 线程任务类只是实现了Runnable接口,可以继续继承其他类,而且可以继续实现其他接口(避免了单继承的局限性)
– 同一个线程任务对象可以被包装成多个线程对象
– 适合多个多个线程去共享同一个资源(后面内容)
– 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立。
– 线程池可以放入实现Runable或Callable线程任务对象。(后面了解)
注意:其实Thread类本身也是实现了Runnable接口的。
– 不能直接得到线程执行的结果!

public class TestDemo {
  public static void main(String[] args) {
    Runnable target = new MyRunnable();

    Thread thread1 = new Thread(target);
    Thread thread2 = new Thread(target,"线程二");

    thread1.start();
    thread2.start();
  }
}
//1. 实现Runnable
class MyRunnable implements Runnable{

  @Override
  public void run() {
    for (int i = 0; i < 10; i++) {
      System.out.println(Thread.currentThread().getName() + "="  + i);
    }
  }
}

27.7 多线程的问题

public class Account {
  private String cardID;
  private double moeny;

  public void drawMoney(double moeny) {
    String name = Thread.currentThread().getName();

    if (this.moeny >= moeny) {
      System.out.println(name + "来取钱"  + moeny);
      this.moeny-=moeny;

      System.out.println(name + "取了钱还剩" + this.moeny);
    }else {
      System.out.println(name + "余额不足");
    }
  }

  public Account(String cardID, double moeny) {
    this.cardID = cardID;
    this.moeny = moeny;
  }

  @Override
  public String toString() {
    return "Account{" +
            "cardID='" + cardID + '\'' +
            ", moeny=" + moeny +
            '}';
  }

  public String getCardID() {
    return cardID;
  }

  public void setCardID(String cardID) {
    this.cardID = cardID;
  }

  public double getMoeny() {
    return moeny;
  }

  public void setMoeny(double moeny) {
    this.moeny = moeny;
  }
}
public class DrawThread extends Thread{
  private Account acc;
  public DrawThread(Account acc,String name) {
    super(name);
    this.acc = acc;
  }


  @Override
  public void run() {
    acc.drawMoney(100000);
  }
}
public class TestDemo {
  public static void main(String[] args) {
    Account acc = new Account("ICBC-110",100000);

    Thread xiaoming = new DrawThread(acc,"小明");
    xiaoming.start();

    Thread xiaohong = new DrawThread(acc,"小红");
    xiaohong.start();
  }
}

同时竞争资源

27.8 线程同步

加上synchronized相当于只有一道门能进去

synchronized (this) {
    if (this.moeny >= moeny) {
        System.out.println(name + "来取钱"  + moeny);
        this.moeny-=moeny;

        System.out.println(name + "取了钱还剩" + this.moeny);
    }else {
        System.out.println(name + "余额不足");
    }
}

加上lock方法使得锁起来

lock.lock(); // 上锁~!
try{
    if(this.money >= money){
        // 3.余额足够
        System.out.println(name+"来取钱,吐出:"+money);
        // 4.更新余额
        this.money -= money;
        // 5.输出结果
        System.out.println(name+"来取钱"+money+"成功,取钱后剩余:"+this.money);
    }else{
        // 6.余额不足
        System.out.println(name+"来取钱,余额不足,剩余"+this.money);
    }
}catch (Exception e){
    e.printStackTrace();
}finally {
    lock.unlock(); // 解锁~!
}

27.9 线程通信

public void wait(): 让当前线程进入到等待状态 此方法必须锁对象调用.
    public void notify() : 唤醒当前锁对象上等待状态的某个线程  此方法必须锁对象调用
    public void notifyAll() : 唤醒当前锁对象上等待状态的全部线程  此方法必须锁对象调用

27.10 线程池

合理利用线程池能够带来三个好处
1.降低资源消耗。
– 减少了创建和销毁线程的次数,每个工作线程都
可以被重复利用,可执行多个任务。

2.提高响应速度
– 不需要频繁的创建线程,如果有
线程可以直接用,不会出现系统僵死!

3.提高线程的可管理性(线程池可以约束系统最多只能有多少个线程,
不会因为线程过多而死机)

ExecutorService pool = Executors.newFixedThreadPool(3);  //创建三个线程池
public class TestDemo {
  public static void main(String[] args) {
    ExecutorService pool = Executors.newFixedThreadPool(3);

    Runnable target = new MyRunnable();

    pool.submit(target);
    pool.submit(target);
    pool.submit(target);
    pool.submit(target);

    pool.shutdown();
  }
}

class MyRunnable implements Runnable{

  @Override
  public void run() {
    for (int i = 0; i < 5; i++) {
      System.out.println(Thread.currentThread().getName() + "==>" + i);
    }
  }
}

27.11 callable执行

public class TestDemo {
  public static void main(String[] args) {
    ExecutorService pool = Executors.newFixedThreadPool(3);  //创建三个线程池

    Future<String> t1 = pool.submit(new MyCallable(100));
    Future<String> t2 = pool.submit(new MyCallable(200));
    Future<String> t3 = pool.submit(new MyCallable(300));
    Future<String> t4 = pool.submit(new MyCallable(400));

    try {
      String s1 = t1.get();
      String s2 = t2.get();
      String s3 = t3.get();
      String s4 = t4.get();

      System.out.println(s1);
      System.out.println(s2);
      System.out.println(s3);
      System.out.println(s4);
    } catch (Exception e) {
      e.printStackTrace();
    }

  }
}

class MyCallable implements Callable<String> {
  private Integer n;

  public MyCallable(Integer n) {
    this.n = n;
  }

  @Override
  public String call() throws Exception {
    long sum = 0;
    for (int i = 0; i <= n; i++) {
      sum += i;
    }
    return Thread.currentThread().getName() + "结果是" + sum;
  }
}

27.12 死锁

两个程序相互竞争资源,互不相让,产生死锁

​ 1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。
​ 2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
​ 3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
​ 4、循环等待,即存在一个等待循环队列:p1要p2的资源,p2要p1的资源。这样就形成了一个等待环路

哲学家进餐问题

必然死锁例子:

public class ThreadDead {
  public static Object resouce01 = new Object();
  public static Object resouce02 = new Object();

  public static void main(String[] args) {
    new Thread(new Runnable() {
      @Override
      public void run() {
        synchronized (resouce01) {
          System.out.println("线程一占用资源一");

          try {
            Thread.sleep(1000);
          } catch (Exception e) {
            e.printStackTrace();
          }

          synchronized (resouce02){
            System.out.println("线程一占用资源二");
          }
        }
      }
    }).start();

    new Thread(new Runnable() {
      @Override
      public void run() {
        synchronized (resouce02) {
          System.out.println("线程二占用资源二");
          try {
            Thread.sleep(1000);
          } catch (Exception e) {
            e.printStackTrace();
          }

          synchronized (resouce01) {
            System.out.println("线程二占用资源一");
          }
        }
      }
    }).start();


  }
}

27.13 volatile关键字

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X3dX1OyD-1640579319704)(F:\人生苦短,我用Java\java进阶13天资料\day08\resources\assets\1576207627535.png)]

可见方式有两种

  • (1)加锁:可以实现其他线程对变量修改的可见性。

    while (true) {
        synchronized (VisiblityDemo.class) {
            if (v.isFlag()) {
                System.out.println("主线程进入了工作");
            }
        }
    }
    
  • (2)可以给成员变量加上一个volatile关键字,立即就实现了成员变量多线程修改的可见性。

    private volatile boolean flag;
    

volatile与synchronized

  • volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。

  • volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制,

二十八、原子性

概述

​ 所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。

public class Demo01 {
  public static void main(String[] args) {
    MyThead myThead = new MyThead();
    for (int i = 0; i < 100; i++) {
      new Thread(myThead).start();
    }
  }
}

class MyThead implements Runnable {
  private int count = 0;

  @Override
  public void run() {

    for (int i = 0; i < 100; i++) {
      count++;
      System.out.println(count);
    }
  }
}

结果不一定是10000

加上volatile也可能不行

28.1 问题解决

  1. 加锁
for (int i = 0; i < 100; i++) {
    synchronized ("mohan"){
        count++;
        System.out.println(count);
    }
}

​ 2. 原子类

概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。

原子型Integer,可以实现原子更新操作

public AtomicInteger():	   				初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer

int get():   			 				 获取值
int getAndIncrement():      			 以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet():     				 以原子方式将当前值加1,注意,这里返回的是自增后的值。
int addAndGet(int data):				 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value):   			 以原子方式设置为newValue的值,并返回旧值。

演示基本使用。

使用原子类

public class VolatileAtomicThread implements Runnable {

    // 定义一个int类型的变量
    private AtomicInteger atomicInteger = new AtomicInteger() ;

    @Override
    public void run() {

        // 对该变量进行++操作,100次
        for(int x = 0 ; x < 100 ; x++) {
            int i = atomicInteger.getAndIncrement();
            System.out.println("count =========>>>> " + i);
        }

    }

}

二十九、并发

29.1 ConcurrentHashMap

  1. Hash线程不安全

    public class TestDemo {
      public static HashMap<String,String> maps = new HashMap<>();
      public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread1 = new Thread(myThread);
        Thread thread2 = new Thread(myThread);
    
        thread1.start();
        thread2.start();
    
        try {
          thread1.join();
          thread2.join();
        } catch (Exception e) {
          e.printStackTrace();
        }
    
    
        System.out.println("Map大小:" + maps.size());
      }
    }
    
    class MyThread implements Runnable{
      
      @Override
      public void run() {
        for (int i = 0; i < 50000; i++) {
          TestDemo.maps.put(Thread.currentThread().getName() + i,i + "");
          System.out.println(Thread.currentThread().getName() + " 结束!");
        }
      }
    }
    
  2. hashtable

    public static Hashtable<String,String> maps = new Hashtable<>();
    

    线程安全,效率低

  3. ConcurrentHashMap

    public static ConcurrentHashMap<String,String> maps = new ConcurrentHashMap<>();
    

    线程安全,效率高,并发执行

29.2 CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作,再执行自己。

例如:线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。

CountDownLatch构造方法:

public CountDownLatch(int count)// 初始化一个指定计数器的CountDownLatch对象

CountDownLatch重要方法:

public void await() throws InterruptedException// 让当前线程等待
public void countDown()	// 计数器进行减1
  • 示例
    1). 制作线程1:
public class ThreadA extends Thread {
    private CountDownLatch down ;
    public ThreadA(CountDownLatch down) {
        this.down = down;
    }
    @Override
    public void run() {
        System.out.println("A");
        try {
            down.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("C");
    }
}

2). 制作线程2:

public class ThreadB extends Thread {
    private CountDownLatch down ;
    public ThreadB(CountDownLatch down) {
        this.down = down;
    }
    @Override
    public void run() {
        System.out.println("B");
        down.countDown();
    }
}

3).制作测试类:

public class Demo {
    public static void main(String[] args) {
        CountDownLatch down = new CountDownLatch(1);//创建1个计数器
        new ThreadA(down).start();
        new ThreadB(down).start();
    }
}

4). 执行结果:
会保证按:A B C的顺序打印。

说明:

CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。

CountDownLatch是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown()方法让计数器-1,当计数器到达0时,调用CountDownLatch。

await()方法的线程阻塞状态解除,继续执行。

29.3 CyclicBarrier

概述

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

例如:公司召集5名员工开会,等5名员工都到了,会议开始。

我们创建5个员工线程,1个开会线程,几乎同时启动,使用CyclicBarrier保证5名员工线程全部执行后,再执行开会线程。

CyclicBarrier构造方法:

public CyclicBarrier(int parties, Runnable barrierAction)// 用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景

CyclicBarrier重要方法:

public int await()// 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
  • 示例代码:
    1). 制作员工线程:
public class PersonThread extends Thread {
	private CyclicBarrier cbRef;
	public PersonThread(CyclicBarrier cbRef) {
		this.cbRef = cbRef;
	}
	@Override
	public void run() {
		try {
			Thread.sleep((int) (Math.random() * 1000));
			System.out.println(Thread.currentThread().getName() + " 到了! ");
			cbRef.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (BrokenBarrierException e) {
			e.printStackTrace();
		}
	}
}

2). 制作开会线程:

public class MeetingThread extends Thread {
    @Override
    public void run() {
        System.out.println("好了,人都到了,开始开会......");
    }
}

3). 制作测试类:

public class Demo {
	public static void main(String[] args) {
		CyclicBarrier cbRef = new CyclicBarrier(5, new MeetingThread());//等待5个线程执行完毕,再执行MeetingThread
		PersonThread p1 = new PersonThread(cbRef);
		PersonThread p2 = new PersonThread(cbRef);
		PersonThread p3 = new PersonThread(cbRef);
		PersonThread p4 = new PersonThread(cbRef);
		PersonThread p5 = new PersonThread(cbRef);
		p1.start();
		p2.start();
		p3.start();
		p4.start();
		p5.start();
	}
}

4). 执行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gG1Pk5oS-1640579319704)(F:\人生苦短,我用Java\java进阶13天资料\day08\resources\imgs\CyclicBarrier_1.png)]

使用场景

使用场景:CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。

需求:使用两个线程读取2个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。

29.4 Semaphore

Semaphore(发信号)的主要作用是控制线程的并发数量。

synchronized可以起到"锁"的作用,但某个时间段内,只能有一个线程允许执行。

Semaphore可以设置同时允许几个线程执行。

Semaphore字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。

Semaphore构造方法:

public Semaphore(int permits)						permits 表示许可线程的数量
public Semaphore(int permits, boolean fair)			fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程

Semaphore重要方法:

public void acquire() throws InterruptedException	表示获取许可
public void release()								release() 表示释放许可
  • 示例一:同时允许1个线程执行

1). 制作一个Service类:

public class Service {
    private Semaphore semaphore = new Semaphore(1);//1表示许可的意思,表示最多允许1个线程执行acquire()和release()之间的内容
    public void testMethod() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()
                    + " 进入 时间=" + System.currentTimeMillis());
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName()
                    + "   结束 时间=" + System.currentTimeMillis());
            semaphore.release();
			//acquire()和release()方法之间的代码为"同步代码"
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2). 制作线程类:

public class ThreadA extends Thread {
	private Service service;
	public ThreadA(Service service) {
		super();
		this.service = service;
	}
	@Override
	public void run() {
		service.testMethod();
	}
}

3). 测试类:

public class Demo {
	public static void main(String[] args) {
		Service service = new Service();
        //启动5个线程
		for (int i = 1; i <= 5; i++) {
			ThreadA a = new ThreadA(service);
			a.setName("线程 " + i);
			a.start();//5个线程会同时执行Service的testMethod方法,而某个时间段只能有1个线程执行
		}
	}
}

4). 结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uXYq2VDu-1640579319705)(F:\人生苦短,我用Java\java进阶13天资料\day08\resources\assets\Semaphore1个线程执行.png)]

  • 示例二:同时允许2个线程同时执行
    1). 修改Service类,将new Semaphore(1)改为2即可:
public class Service {
    private Semaphore semaphore = new Semaphore(2);//2表示许可的意思,表示最多允许2个线程执行acquire()和release()之间的内容
    public void testMethod() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()
                    + " 进入 时间=" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName()
                    + "   结束 时间=" + System.currentTimeMillis());
            semaphore.release();
			//acquire()和release()方法之间的代码为"同步代码"
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2). 再次执行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PhWXLL3m-1640579319705)(F:\人生苦短,我用Java\java进阶13天资料\day08\resources\assets\Semaphore2个线程执行.png)]

29.5 Exchanger

概述

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。

这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

Exchanger构造方法:

public Exchanger()

Exchanger重要方法:

public V exchange(V x)
  • 示例一:exchange方法的阻塞特性

1).制作线程A,并能够接收一个Exchanger对象:

public class ThreadA extends Thread {
	private Exchanger<String> exchanger;
	public ThreadA(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程A欲传递值'礼物A'给线程B,并等待线程B的值...");
			System.out.println("在线程A中得到线程B的值=" + exchanger.exchange("礼物A"));

		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}

2). 制作main()方法:

public class Demo {
	public static void main(String[] args) {
		Exchanger<String> exchanger = new Exchanger<String>();
		ThreadA a = new ThreadA(exchanger);
		a.start();
	}
}

3).执行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2NGelmOk-1640579319706)(F:\人生苦短,我用Java\java进阶13天资料\day08\resources\assets\Exchange阻塞.png)]

  • 示例二:exchange方法执行交换

1).制作线程A:

public class ThreadA extends Thread {
	private Exchanger<String> exchanger;
	public ThreadA(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程A欲传递值'礼物A'给线程B,并等待线程B的值...");
			System.out.println("在线程A中得到线程B的值=" + exchanger.exchange("礼物A"));
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

2).制作线程B:

public class ThreadB extends Thread {
	private Exchanger<String> exchanger;
	public ThreadB(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程B欲传递值'礼物B'给线程A,并等待线程A的值...");
			System.out.println("在线程B中得到线程A的值=" + exchanger.exchange("礼物B"));

		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

3).制作测试类:

public class Demo {
	public static void main(String[] args) throws InterruptedException {
		Exchanger<String> exchanger = new Exchanger<String>();
		ThreadA a = new ThreadA(exchanger);
		ThreadB b = new ThreadB(exchanger);
		a.start();
		b.start();
	}
}

4).执行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xXWBZkBI-1640579319706)(F:\人生苦短,我用Java\java进阶13天资料\day08\resources\assets\Exchange交换.png)]

  • 示例三:exchange方法的超时

1).制作线程A:

public class ThreadA extends Thread {
	private Exchanger<String> exchanger;
	public ThreadA(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程A欲传递值'礼物A'给线程B,并等待线程B的值,只等5秒...");
			System.out.println("在线程A中得到线程B的值 =" + exchanger.exchange("礼物A",5, TimeUnit.SECONDS));
			System.out.println("线程A结束!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (TimeoutException e) {
			System.out.println("5秒钟没等到线程B的值,线程A结束!");
		}
	}
}

2).制作测试类:

public class Run {
	public static void main(String[] args) {
		Exchanger<String> exchanger = new Exchanger<String>();
		ThreadA a = new ThreadA(exchanger);
		a.start();
	}
}

3).测试结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EOXdNqRr-1640579319706)(F:\人生苦短,我用Java\java进阶13天资料\day08\resources\assets\Exchange超时.png)]

使用场景

使用场景:可以做数据校对工作

需求:比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水。为了避免错误,采用AB岗两人进行录入,录入到两个文件中,系统需要加载这两个文件,

并对两个文件数据进行校对,看看是否录入一致,

三十、Lambda表达式

概述

lambda表达式是JDK1.8之后开发的新技术,是一种代码的新语法

作用

核心目的是为了简化匿名内部类的代码写法

30.1 Runnable

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+":执行~~~");
    }
});
thread.start();

new Thread(() -> {
    System.out.println(Thread.currentThread().getName()+":执行~~~");
}).start();

new Thread(() -> System.out.println(Thread.currentThread().getName()+":执行~~~")).start();

30.2 Compared

List<Student> lists = new ArrayList<>();
Student s1 = new Student("李铭",18,'男');
Student s2 = new Student("冯龙",23,'男');
Student s3 = new Student("王乐乐",21,'男');
Collections.addAll(lists,s1,s2,s3);

Collections.sort(lists, new Comparator<Student>() {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getAge()-o2.getAge();
    }
});

Collections.sort(lists,((o1, o2) -> o1.getAge()-o2.getAge()));

30.3 foreach

List<String> names = new ArrayList<>();
names.add("胡伟光");
names.add("甘挺");
names.add("洪磊");

names.forEach(new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
});

names.forEach((String s) -> {
    System.out.println(s);
});

names.forEach((s) -> {
    System.out.println(s);
});

names.forEach(s -> {
    System.out.println(s);
});

names.forEach(s -> System.out.println(s) );

names.forEach(System.out::println);

三十一、引用方法

31.1 静态调用

前面和后面都只有一个参数

List<String> list = new ArrayList<>();
Collections.addAll(list,"莫寒","ccf","wc","陌陌");
list.forEach(System.out::println);
List<Student> lists = new ArrayList<>();
Student s1 = new Student("李铭",18,'男');
Student s2 = new Student("冯龙",23,'男');
Student s3 = new Student("王乐乐",21,'男');
Collections.addAll(lists , s1 , s2 , s3);

Collections.sort(lists,Student::compareByAge);

System.out.println(lists);

31.2 实例方法

.实例方法的引用
格式: 对象::实例方法。
简化步骤:
a.定义一个实例方法,把需要的代码放到实例方法中去。
实例方法引用的注意事项
” 重要:被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致。“

List<Student> lists = new ArrayList<>();
Student s1 = new Student("李铭",18,'男');
Student s2 = new Student("冯龙",23,'男');
Student s3 = new Student("王乐乐",21,'男');
Collections.addAll(lists , s1 , s2 , s3);

Collections.sort(lists,Student::compareByAge);

三十二、Steam流

32.1 什么是Steam流

在Java 8中,得益于Lambda所带来的函数式编程,
引入了一个全新的Stream流概念 ,用于解决已有集合/数组类库有的弊端。

32.2 常用方法

流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:

  • 终结方法:返回值类型不再是Stream接口自身类型的方法,因此不再支持类似StringBuilder那样的链式调用。本小节中,终结方法包括countforEach方法。
  • 非终结方法:返回值类型仍然是Stream接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为非终结方法。)

函数拼接与终结方法

在上述介绍的各种方法中,凡是返回值仍然为Stream接口的为函数拼接方法,它们支持链式调用;而返回值不再为Stream接口的为终结方法,不再支持链式调用。如下表所示:

方法名方法作用方法种类是否支持链式调用
count统计个数终结
forEach逐一处理终结
filter过滤函数拼接
limit取用前几个函数拼接
skip跳过前几个函数拼接
map映射函数拼接
concat组合函数拼接

备注:本小节之外的更多方法,请自行参考API文档。

forEach : 逐一处理

虽然方法名字叫forEach,但是与for循环中的“for-each”昵称不同,该方法并不保证元素的逐一消费动作在流中是被有序执行的

void forEach(Consumer<? super T> action);

该方法接收一个Consumer接口函数,会将每一个流元素交给该函数进行处理。例如:

import java.util.stream.Stream;

public class Demo12StreamForEach {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("张无忌", "张三丰", "周芷若");
        stream.forEach(s->System.out.println(s));
    }
}

count:统计个数

正如旧集合Collection当中的size方法一样,流提供count方法来数一数其中的元素个数:

long count();

该方法返回一个long值代表元素个数(不再像旧集合那样是int值)。基本使用:

public class Demo09StreamCount {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.filter(s -> s.startsWith("张"));
        System.out.println(result.count()); // 2
    }
}

filter:过滤

可以通过filter方法将一个流转换成另一个子集流。方法声明:

Stream<T> filter(Predicate<? super T> predicate);

该接口接收一个Predicate函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。

基本使用

Stream流中的filter方法基本使用的代码如:

public class Demo07StreamFilter {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.filter(s -> s.startsWith("张"));
    }
}

在这里通过Lambda表达式来指定了筛选的条件:必须姓张。

limit:取用前几个

limit方法可以对流进行截取,只取用前n个。方法签名:

Stream<T> limit(long maxSize);

参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。基本使用:

import java.util.stream.Stream;

public class Demo10StreamLimit {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.limit(2);
        System.out.println(result.count()); // 2
    }
}

skip:跳过前几个

如果希望跳过前几个元素,可以使用skip方法获取一个截取之后的新流:

Stream<T> skip(long n);

如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:

import java.util.stream.Stream;

public class Demo11StreamSkip {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.skip(2);
        System.out.println(result.count()); // 1
    }
}

map:映射

如果需要将流中的元素映射到另一个流中,可以使用map方法。方法签名:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

该接口需要一个Function函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。

基本使用

Stream流中的map方法基本使用的代码如:

import java.util.stream.Stream;

public class Demo08StreamMap {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("10", "12", "18");
        Stream<Integer> result = original.map(s->Integer.parseInt(s));
    }
}

这段代码中,map方法的参数通过方法引用,将字符串类型转换成为了int类型(并自动装箱为Integer类对象)。

concat:组合

如果有两个流,希望合并成为一个流,那么可以使用Stream接口的静态方法concat

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

备注:这是一个静态方法,与java.lang.String当中的concat方法是不同的。

该方法的基本使用代码如:

import java.util.stream.Stream;

public class Demo12StreamConcat {
    public static void main(String[] args) {
        Stream<String> streamA = Stream.of("张无忌");
        Stream<String> streamB = Stream.of("张翠山");
        Stream<String> result = Stream.concat(streamA, streamB);
    }
}

三十三、 File类

33.1 概述

java.io.File 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。

33.2 构造方法

  • public File(String pathname) :通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例。

  • public File(String parent, String child) :从父路径名字符串和子路径名字符串创建新的 File实例。

  • public File(File parent, String child) :从父抽象路径名和子路径名字符串创建新的 File实例。

  • 构造举例,代码如下:

// 文件路径名
String pathname = "D:\\aaa.txt";
File file1 = new File(pathname); 

// 文件路径名
String pathname2 = "D:\\aaa\\bbb.txt";
File file2 = new File(pathname2); 

// 通过父路径和子路径字符串
 String parent = "d:\\aaa";
 String child = "bbb.txt";
 File file3 = new File(parent, child);

// 通过父级File对象和子路径字符串
File parentDir = new File("d:\\aaa");
String child = "bbb.txt";
File file4 = new File(parentDir, child);

小贴士:

  1. 一个File对象代表硬盘中实际存在的一个文件或者目录。
  2. 无论该路径下是否存在文件或者目录,都不影响File对象的创建。

33.3 常用方法

获取功能的方法

  • public String getAbsolutePath() :返回此File的绝对路径名字符串。

  • public String getPath() :将此File转换为路径名字符串。

  • public String getName() :返回由此File表示的文件或目录的名称。

  • public long length() :返回由此File表示的文件的长度。

    方法演示,代码如下:

    public class FileGet {
        public static void main(String[] args) {
            File f = new File("d:/aaa/bbb.java");     
            System.out.println("文件绝对路径:"+f.getAbsolutePath());
            System.out.println("文件构造路径:"+f.getPath());
            System.out.println("文件名称:"+f.getName());
            System.out.println("文件长度:"+f.length()+"字节");
    
            File f2 = new File("d:/aaa");     
            System.out.println("目录绝对路径:"+f2.getAbsolutePath());
            System.out.println("目录构造路径:"+f2.getPath());
            System.out.println("目录名称:"+f2.getName());
            System.out.println("目录长度:"+f2.length());
        }
    }
    输出结果:
    文件绝对路径:d:\aaa\bbb.java
    文件构造路径:d:\aaa\bbb.java
    文件名称:bbb.java
    文件长度:636字节
    
    目录绝对路径:d:\aaa
    目录构造路径:d:\aaa
    目录名称:aaa
    目录长度:4096
    

API中说明:length(),表示文件的长度。但是File对象表示目录,则返回值未指定。

绝对路径和相对路径

  • 绝对路径:从盘符开始的路径,这是一个完整的路径。
  • 相对路径:相对于项目目录的路径,这是一个便捷的路径,开发中经常使用。
public class FilePath {
    public static void main(String[] args) {
      	// D盘下的bbb.java文件
        File f = new File("D:\\bbb.java");
        System.out.println(f.getAbsolutePath());
      	
		// 项目下的bbb.java文件
        File f2 = new File("bbb.java");
        System.out.println(f2.getAbsolutePath());
    }
}
输出结果:
D:\bbb.java
D:\idea_project_test4\bbb.java

判断功能的方法

  • public boolean exists() :此File表示的文件或目录是否实际存在。
  • public boolean isDirectory() :此File表示的是否为目录。
  • public boolean isFile() :此File表示的是否为文件。

方法演示,代码如下:

public class FileIs {
    public static void main(String[] args) {
        File f = new File("d:\\aaa\\bbb.java");
        File f2 = new File("d:\\aaa");
      	// 判断是否存在
        System.out.println("d:\\aaa\\bbb.java 是否存在:"+f.exists());
        System.out.println("d:\\aaa 是否存在:"+f2.exists());
      	// 判断是文件还是目录
        System.out.println("d:\\aaa 文件?:"+f2.isFile());
        System.out.println("d:\\aaa 目录?:"+f2.isDirectory());
    }
}
输出结果:
d:\aaa\bbb.java 是否存在:true
d:\aaa 是否存在:true
d:\aaa 文件?:false
d:\aaa 目录?:true

创建删除功能的方法

  • public boolean createNewFile() :当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。
  • public boolean delete() :删除由此File表示的文件或目录。
  • public boolean mkdir() :创建由此File表示的目录。
  • public boolean mkdirs() :创建由此File表示的目录,包括任何必需但不存在的父目录。

方法演示,代码如下:

public class FileCreateDelete {
    public static void main(String[] args) throws IOException {
        // 文件的创建
        File f = new File("aaa.txt");
        System.out.println("是否存在:"+f.exists()); // false
        System.out.println("是否创建:"+f.createNewFile()); // true
        System.out.println("是否存在:"+f.exists()); // true
		
     	// 目录的创建
      	File f2= new File("newDir");	
        System.out.println("是否存在:"+f2.exists());// false
        System.out.println("是否创建:"+f2.mkdir());	// true
        System.out.println("是否存在:"+f2.exists());// true

		// 创建多级目录
      	File f3= new File("newDira\\newDirb");
        System.out.println(f3.mkdir());// false
        File f4= new File("newDira\\newDirb");
        System.out.println(f4.mkdirs());// true
      
      	// 文件的删除
       	System.out.println(f.delete());// true
      
      	// 目录的删除
        System.out.println(f2.delete());// true
        System.out.println(f4.delete());// false
    }
}

API中说明:delete方法,如果此File表示目录,则目录必须为空才能删除。

33.4 目录的遍历

  • public String[] list() :返回一个String数组,表示该File目录中的所有子文件或目录。

  • public File[] listFiles() :返回一个File数组,表示该File目录中的所有的子文件或目录。

public class FileFor {
    public static void main(String[] args) {
        File dir = new File("d:\\java_code");
      
      	//获取当前目录下的文件以及文件夹的名称。
		String[] names = dir.list();
		for(String name : names){
			System.out.println(name);
		}
        //获取当前目录下的文件以及文件夹对象,只要拿到了文件对象,那么就可以获取更多信息
        File[] files = dir.listFiles();
        for (File file : files) {
            System.out.println(file);
        }
    }
}

小贴士:

调用listFiles方法的File对象,表示的必须是实际存在的目录,否则返回null,无法进行遍历。

三十四、递归

34.1 概述

  • 递归:指在当前方法内调用自己的这种现象。
public static void a(){
    a();
}

34.2 递归累和

计算1 ~ n的和

分析:num的累和 = num + (num-1)的累和,所以可以把累和的操作定义成一个方法,递归调用。

实现代码

public class DiGuiDemo {
	public static void main(String[] args) {
		//计算1~num的和,使用递归完成
		int num = 5;
      	// 调用求和的方法
		int sum = getSum(num);
      	// 输出结果
		System.out.println(sum);
		
	}
  	/*
  	  通过递归算法实现.
  	  参数列表:int 
  	  返回值类型: int 
  	*/
	public static int getSum(int num) {
      	/* 
      	   num为1时,方法返回1,
      	   相当于是方法的出口,num总有是1的情况
      	*/
		if(num == 1){
			return 1;
		}
      	/*
          num不为1时,方法返回 num +(num-1)的累和
          递归调用getSum方法
        */
		return num + getSum(num-1);
	}
}

代码执行图解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GMcfphK8-1640579319707)(F:\人生苦短,我用Java\java进阶13天资料\day09\resources\assets\day08_01_递归累和.jpg)]

小贴士:递归一定要有条件限定,保证递归能够停止下来,次数不要太多,否则会发生栈内存溢出。

34.3 递归求阶乘

  • 阶乘:所有小于及等于该数的正整数的积。
n的阶乘:n! = n * (n-1) *...* 3 * 2 * 1 

分析:这与累和类似,只不过换成了乘法运算,学员可以自己练习,需要注意阶乘值符合int类型的范围。

推理得出:n! = n * (n-1)!

代码实现

public class DiGuiDemo {
  	//计算n的阶乘,使用递归完成
    public static void main(String[] args) {
        int n = 3;
      	// 调用求阶乘的方法
        int value = getValue(n);
      	// 输出结果
        System.out.println("阶乘为:"+ value);
    }
	/*
  	  通过递归算法实现.
  	  参数列表:int 
  	  返回值类型: int 
  	*/
    public static int getValue(int n) {
      	// 1的阶乘为1
        if (n == 1) {
            return 1;
        }
      	/*
      	  n不为1时,方法返回 n! = n*(n-1)!
          递归调用getValue方法
      	*/
        return n * getValue(n - 1);
    }
}

34.4 文件搜索

搜索D:\aaa 目录中的.java 文件。

分析

  1. 目录搜索,无法判断多少级目录,所以使用递归,遍历所有目录。
  2. 遍历目录时,获取的子文件,通过文件名称,判断是否符合条件。

代码实现

public class DiGuiDemo3 {
    public static void main(String[] args) {
        // 创建File对象
        File dir  = new File("D:\\aaa");
      	// 调用打印目录方法
        printDir(dir);
    }

    public static void printDir(File dir) {
      	// 获取子文件和目录
        File[] files = dir.listFiles();
      	
      	// 循环打印
        for (File file : files) {
            if (file.isFile()) {
              	// 是文件,判断文件名并输出文件绝对路径
                if (file.getName().endsWith(".java")) {
                    System.out.println("文件名:" + file.getAbsolutePath());
                }
            } else {
                // 是目录,继续遍历,形成递归
                printDir(file);
            }
        }
    }
}

三十六、计算机网络

36.1 网络通信协议

  • **网络通信协议:**通信协议是对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信。这就好比在道路中行驶的汽车一定要遵守交通规则一样,协议中对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守,最终完成数据交换。
  • TCP/IP协议: 传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol),是Internet最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aFYySUNK-1640579319707)(F:\人生苦短,我用Java\images\03_通讯协议.png)]

36.2 协议分类

通信的协议还是比较复杂的,java.net 包中包含的类和接口,它们提供低层次的通信细节。我们可以直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信的细节。

java.net 包中提供了两种常见的网络协议的支持:

  • TCP:传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。

    • 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
      • 第一次握手,客户端向服务器端发出连接请求,等待服务器确认。服务器你死了吗?
      • 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。我活着啊!!
      • 第三次握手,客户端再次向服务器端发送确认信息,确认连接。整个交互过程如下图所示。我知道了!!

    完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。

  • UDP:用户数据报协议(User Datagram Protocol)。UDP协议是一个面向无连接的协议。传输数据时,不需要建立连接,不管对方端服务是否启动,直接将数据、数据源和目的地都封装在数据包中,直接发送。每个数据包的大小限制在64k以内。它是不可靠协议,因为无连接,所以传输速度快,但是容易丢失数据。日常应用中,例如视频会议、QQ聊天等。

36.3 网络编程三要素

协议

  • **协议:**计算机网络通信必须遵守的规则,已经介绍过了,不再赘述。

IP地址

  • IP地址:指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”。

**IP地址分类 ** [][][][] o o o o

  • IPv4:是一个32位的二进制数,通常被分为4个字节,表示成a.b.c.d 的形式,例如192.168.65.100 。其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。

  • IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。有资料显示,全球IPv4地址在2011年2月分配完毕。

    为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成ABCD:EF01:2345:6789:ABCD:EF01:2345:6789,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。

常用命令

  • 查看本机IP地址,在控制台输入:
ipconfig
  • 检查网络是否连通,在控制台输入:
ping 空格 IP地址
ping 220.181.57.216
ping www.baidu.com

特殊的IP地址

  • 本机IP地址:127.0.0.1localhost

端口号

网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?

如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。

  • **端口号:用两个字节表示的整数,它的取值范围是065535**。其中,01023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。

利用协议+IP地址+端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。ddress

/**
    InetAddress类概述
        * 一个该类的对象就代表一个IP地址对象。

    InetAddress类成员方法
        * static InetAddress getLocalHost()
            * 获得本地主机IP地址对象
        * static InetAddress getByName(String host)
            * 根据IP地址字符串或主机名获得对应的IP地址对象

        * String getHostName();获得主机名
        * String getHostAddress();获得IP地址字符串
 */

try {
    InetAddress inetAddress = InetAddress.getLocalHost();
    System.out.println(inetAddress);
    InetAddress address = InetAddress.getByName("www.mohanhan.cn");
    System.out.println(address);
    InetAddress name = InetAddress.getByName("baidu.com");
    System.out.println(name);

    String hostName = address.getHostName();
    String address1 = address.getHostAddress();
    System.out.println(hostName);
    System.out.println(address1);
} catch (UnknownHostException e) {
    e.printStackTrace();
}

36.4 UDP通信程序

概述

​ UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。

​ 由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输例如视频会议都使用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。

​ 但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。UDP通信过程如下图所示:

UDP协议的特点
    * 面向无连接的协议
    * 发送端只管发送,不确认对方是否能收到。
    * 基于数据包进行数据传输。
    * 发送数据的大小限制64K以内
    * 因为面向无连接,速度快,但是不可靠。

UDP协议的使用场景
    * 即时通讯
    * 在线视频
    * 网络语音电话

UDP协议相关的两个类
    * DatagramPacket
        * 数据包对象
        * 作用:用来封装要发送或要接收的数据,比如:集装箱
    * DategramSocket
        * 发送对象
        * 作用:用来发送或接收数据包,比如:码头

DatagramPacket类构造方法
    * DatagramPacket(byte[] buf, int length, InetAddress address, int port)
        * 创建发送端数据包对象
        * buf:要发送的内容,字节数组
        * length:要发送内容的长度,单位是字节
        * address:接收端的IP地址对象
        * port:接收端的端口号
    * DatagramPacket(byte[] buf, int length)
        * 创建接收端的数据包对象
        * buf:用来存储接收到内容
        * length:能够接收内容的长度

DatagramPacket类常用方法
    * int getLength() 获得实际接收到的字节个数

DatagramSocket类构造方法
    * DatagramSocket() 创建发送端的Socket对象,系统会随机分配一个端口号。
    * DatagramSocket(int port) 创建接收端的Socket对象并指定端口号

DatagramSocket类成员方法
    * void send(DatagramPacket dp) 发送数据包
    * void receive(DatagramPacket p) 接收数据包

36.5 UDP通信案例

  • 需求:教师的电脑的一个程序发送数据,一个程序接收数据,使用的教师本机的ip。

36.5.1 UDP发送端代码实现

ir = new File(“D:\aaa”);
// 调用打印目录方法
printDir(dir);
}

public static void printDir(File dir) {
  	// 获取子文件和目录
    File[] files = dir.listFiles();
  	
  	// 循环打印
    for (File file : files) {
        if (file.isFile()) {
          	// 是文件,判断文件名并输出文件绝对路径
            if (file.getName().endsWith(".java")) {
                System.out.println("文件名:" + file.getAbsolutePath());
            }
        } else {
            // 是目录,继续遍历,形成递归
            printDir(file);
        }
    }
}

}




#  三十六、计算机网络

## 36.1 网络通信协议

- **网络通信协议:**通信协议是对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信。这就好比在道路中行驶的汽车一定要遵守交通规则一样,协议中对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守,最终完成数据交换。 
- **TCP/IP协议:** 传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol),是Internet最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求。

[外链图片转存中...(img-aFYySUNK-1640579319707)]



## 36.2 协议分类

通信的协议还是比较复杂的,`java.net` 包中包含的类和接口,它们提供低层次的通信细节。我们可以直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信的细节。

`java.net` 包中提供了两种常见的网络协议的支持:

- **TCP**:传输控制协议 (Transmission Control Protocol)。TCP协议是**面向连接**的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。
  - 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
    - 第一次握手,客户端向服务器端发出连接请求,等待服务器确认。服务器你死了吗?
    - 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。我活着啊!!
    - 第三次握手,客户端再次向服务器端发送确认信息,确认连接。整个交互过程如下图所示。我知道了!!





   完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。

- **UDP**:用户数据报协议(User Datagram Protocol)。UDP协议是一个**面向无连接**的协议。传输数据时,不需要建立连接,不管对方端服务是否启动,直接将数据、数据源和目的地都封装在数据包中,直接发送。每个数据包的大小限制在64k以内。它是不可靠协议,因为无连接,所以传输速度快,但是容易丢失数据。日常应用中,例如视频会议、QQ聊天等。



## 36.3 网络编程三要素

### 协议

- **协议:**计算机网络通信必须遵守的规则,已经介绍过了,不再赘述。

### IP地址

- **IP地址:指互联网协议地址(Internet Protocol Address)**,俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”。

**IP地址分类 **   [][][][] o o o o

- IPv4:是一个32位的二进制数,通常被分为4个字节,表示成`a.b.c.d` 的形式,例如`192.168.65.100` 。其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。

- IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。有资料显示,全球IPv4地址在2011年2月分配完毕。

  为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成`ABCD:EF01:2345:6789:ABCD:EF01:2345:6789`,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。

**常用命令**

- 查看本机IP地址,在控制台输入:

```java
ipconfig
  • 检查网络是否连通,在控制台输入:
ping 空格 IP地址
ping 220.181.57.216
ping www.baidu.com

特殊的IP地址

  • 本机IP地址:127.0.0.1localhost

端口号

网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?

如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。

  • **端口号:用两个字节表示的整数,它的取值范围是065535**。其中,01023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。

利用协议+IP地址+端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。ddress

/**
    InetAddress类概述
        * 一个该类的对象就代表一个IP地址对象。

    InetAddress类成员方法
        * static InetAddress getLocalHost()
            * 获得本地主机IP地址对象
        * static InetAddress getByName(String host)
            * 根据IP地址字符串或主机名获得对应的IP地址对象

        * String getHostName();获得主机名
        * String getHostAddress();获得IP地址字符串
 */

try {
    InetAddress inetAddress = InetAddress.getLocalHost();
    System.out.println(inetAddress);
    InetAddress address = InetAddress.getByName("www.mohanhan.cn");
    System.out.println(address);
    InetAddress name = InetAddress.getByName("baidu.com");
    System.out.println(name);

    String hostName = address.getHostName();
    String address1 = address.getHostAddress();
    System.out.println(hostName);
    System.out.println(address1);
} catch (UnknownHostException e) {
    e.printStackTrace();
}

36.4 UDP通信程序

概述

​ UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。

​ 由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输例如视频会议都使用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。

​ 但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。UDP通信过程如下图所示:

UDP协议的特点
    * 面向无连接的协议
    * 发送端只管发送,不确认对方是否能收到。
    * 基于数据包进行数据传输。
    * 发送数据的大小限制64K以内
    * 因为面向无连接,速度快,但是不可靠。

UDP协议的使用场景
    * 即时通讯
    * 在线视频
    * 网络语音电话

UDP协议相关的两个类
    * DatagramPacket
        * 数据包对象
        * 作用:用来封装要发送或要接收的数据,比如:集装箱
    * DategramSocket
        * 发送对象
        * 作用:用来发送或接收数据包,比如:码头

DatagramPacket类构造方法
    * DatagramPacket(byte[] buf, int length, InetAddress address, int port)
        * 创建发送端数据包对象
        * buf:要发送的内容,字节数组
        * length:要发送内容的长度,单位是字节
        * address:接收端的IP地址对象
        * port:接收端的端口号
    * DatagramPacket(byte[] buf, int length)
        * 创建接收端的数据包对象
        * buf:用来存储接收到内容
        * length:能够接收内容的长度

DatagramPacket类常用方法
    * int getLength() 获得实际接收到的字节个数

DatagramSocket类构造方法
    * DatagramSocket() 创建发送端的Socket对象,系统会随机分配一个端口号。
    * DatagramSocket(int port) 创建接收端的Socket对象并指定端口号

DatagramSocket类成员方法
    * void send(DatagramPacket dp) 发送数据包
    * void receive(DatagramPacket p) 接收数据包

36.5 UDP通信案例

  • 需求:教师的电脑的一个程序发送数据,一个程序接收数据,使用的教师本机的ip。

36.5.1 UDP发送端代码实现

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我会尽力回答你关于Java Socket网络编程的问题。 Java Socket网络编程是Java语言中常用的网络通信技术之一,它可以实现客户端与服务端之间的通信,常用于开发网络应用程序、网络游戏等。下面我们来看一个基于Java Socket网络编程的应用实例。 假设我们有一个简单的聊天室应用程序,它由一个服务端和多个客户端组成。服务端负责接收客户端连接请求,并将客户端发送的消息广播给所有连接到服务端的客户端,客户端负责向服务端发送消息并接收来自服务端的消息。 1. 服务端实现 首先,我们需要编写服务端的代码。服务端需要监听指定的端口,等待客户端的连接请求,并在接收到连接请求时为每个客户端创建一个独立的线程进行处理。以下是服务端的代码示例: ```java import java.io.*; import java.net.*; public class Server { public static void main(String[] args) throws IOException { ServerSocket serverSocket = null; boolean listening = true; try { serverSocket = new ServerSocket(4444); } catch (IOException e) { System.err.println("Could not listen on port: 4444."); System.exit(-1); } while (listening) { new ServerThread(serverSocket.accept()).start(); } serverSocket.close(); } } class ServerThread extends Thread { private Socket socket = null; public ServerThread(Socket socket) { super("ServerThread"); this.socket = socket; } public void run() { try { PrintWriter out = new PrintWriter(socket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); String inputLine; while ((inputLine = in.readLine()) != null) { out.println(inputLine); } out.close(); in.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } } ``` 2. 客户端实现 接下来,我们需要编写客户端的代码。客户端需要连接到服务端指定的端口,并在连接成功后启动一个单独的线程用于接收来自服务端的消息。以下是客户端的代码示例: ```java import java.io.*; import java.net.*; public class Client implements Runnable { private static Socket socket = null; public static void main(String[] args) throws IOException { String hostName = "localhost"; int portNumber = 4444; try { socket = new Socket(hostName, portNumber); } catch (UnknownHostException e) { System.err.println("Don't know about host " + hostName); System.exit(1); } catch (IOException e) { System.err.println("Couldn't get I/O for the connection to " + hostName); System.exit(1); } BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in)); new Thread(new Client()).start(); String userInput; while ((userInput = stdIn.readLine()) != null) { PrintWriter out = new PrintWriter(socket.getOutputStream(), true); out.println(userInput); if (userInput.equals("exit")) { break; } } socket.close(); } public void run() { try { BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); String inputLine; while ((inputLine = in.readLine()) != null) { System.out.println("Server: " + inputLine); } in.close(); } catch (IOException e) { e.printStackTrace(); } } } ``` 以上就是一个简单的基于Java Socket网络编程的聊天室应用程序的实现。在实际开发中,我们可以根据具体需求对代码进行修改和优化,使其更符合实际业务需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值