史上最全的Java学习路线

基础 专栏收录该内容
0 篇文章 0 订阅

java基础

一、Java的历史和三大版本

1. Java的发展史

 Java由sun公司开发,java之父 James Gosling Java是一门面向对象的编程语言,也称为“高级编程语言” 
Java在1994年由sun公司推出,Java的前身叫oak语言,开源,免费
Java的版本从JDK1.0开始 到目前的JDK13 
目前开发常用版本: JDK8.0、JDK9.0 

2. Java语言能做什么

Java语言主要用于互联网应用程序开发,例如 天猫、京东、 大型公司的项目管理系统、手机APP的后台数据支撑系统 ,主要开发web系统(基于浏览器访问的) ,还有服务器后台存储的数据分析,数据查询等相关系统。
  1. Java的版本分类

    a、Java SE (J2SE) Java PlatForm Standard Edition Java的标准版本(称为Java基础)
    b、JavaEE (J2EE) Java PlatForm Enterprise Edition Java的企业版本
    c、JavaME (J2ME) Java PlatForm Microsoft Edition Java的微型版本
    在这里插入图片描述

二、Java的开发环境

1. 配置开发环境

   第一步 下载jdk 并安装 ,建议安装在默认c盘 官网下载
  第二步 配置环境变量
我的电脑-》 属性-》 高级系统设置-》 环境变量 --》 新建 一个系统变量 ,变量名
JAVA_HOME : C:\Program Files\Java\jdk1.8.0_144
在path路径下 ,配置 %JAVA_HOME%\bin , 需要将该路径放置path的最前面
或者直接在path下配置 “C:\Program Files\Java\jdk1.8.0_144\bin”
第三步:测试
win+r : 运行窗口 中输入cmd

输入 Java
在这里插入图片描述

输入 javac
在这里插入图片描述

环境变量配置成功

三、Java的第一个程序及运行原理

  Java程序是一个 以.java结尾的文件 , 称为“源程序”
  Java程序需要运行 ,必须经过两个步骤: 先编译 再运行,将源程序先编译成.class文件 ,编译后的class文件称为“字节码”文件 ,然后再由Java虚拟机(JVM)运行 文件,最后得到结果 。
在这里插入图片描述

  JDK : Java Development Kit Java开发工具 ,包括Java开发中 运用的所有工具(包,接口API等,运行环境等)
  JRE :Java Runtime Environment Java运行环境 , 运行Java程序需要的相关依赖
  JVM : Java Virtual Mechine Java虚拟机器, 用于运行Java程序实现跨平台虚拟计算机。
在这里插入图片描述

JVM 的组成部分

  1、寄存器(程序计数器)
  2、本地方法区
  3、堆区(堆内存)
  4 、栈区(栈内存)
  5、 方法区

写第一个Java程序

1、新建HelloWorld.java 文件

2、编写Java程序 ( 文件名与类名 保持一致)

public class HelloWorld{
    // 这里是程序的入口  main函数
    public static void main(String [] args){
        System.out.println("hello word!!!!");
    }
}

3、编译java程序 (Compile)

  在文件所在的目录下输入cmd ,打开命令行窗口 输入

  javac HelloWorld.java

4、运行java程序

  java HelloWorld (注意这里没有后缀.java )
注意 :对于中文输出乱码,需要 另存为一下,修改编码格式为ANSI 即可
  Java文件 一次编译多次运行
  Java的运行原理图:
在这里插入图片描述

四、Java的开发工具

  Java开发使用集成开发环境,一般企业使用居多的 eclipse 或 IDEA 开发工具

idea常用快捷键

  alt+enter : 自动导入包, 代码自动修正
  ctrl+d :将当前光标 所在行 复制到下一行
  ctrl+ y : 删除光标所在的 当前行
  ctrl+alt+l : 格式化代码
  ctrl+ / : 当行注释
  ctrl+shift+/ : 文档注释

创建项目:

  方式一: 直接创建项目 在src下创建包和类文件 ,每次创建项目都是独立窗口
  方式二: 先创建一个空项目(Empty Project) ,再创建子模块Module ,好处是一个项目下可以存放多个子模块
给文件增加文件头注释

/** 
* @Author: wuyafeng by softeem
* @Date: ${DATE} ${TIME} 
* @Description: 
* 
*/    

在这里插入图片描述

Java的注释 分为三种

  1、单行注释(ctrl+/) : 用于对某一句代码的注释
  2、多行注释(ctrl+shift+/): 用于对一段代码的注释
  3、文档注释 (/** + enter) : 注释一个类 或 一个方法, 或一个语句块 ,文档注释可以自动生成API文档 javadoc 命令

五、Java的关键字和标识符

关键字

1、定义:

  在Java程序中,已经定义好的被预先使用的一些特殊的单词称为关键字 ,一共有50个关键字 (48+2个保留字) ,关键字都是小写的英文单词

2、关键字的分类

2.1 数据类型关键字

  byte :字节类型
  short : 短整型
  int : 整型
  long: 长整型
  float :单精度浮点型
  double:双精度浮点型
  char: 字符型
  boolean : 布尔类型
  void :空类型
  null :空对象

2.2 流程控制关键字

  if: 条件分支判断
  else:条件分支
  switch: 条件分支判断
  case : 条件分支其中一种情况
  default : 默认
  break: 退出条件 或 循环
  continue : 退出当前循环,继续下一次循环
  for : 循环
  do: 循环
  while : 循环
  return:方法的返回

2.3 面向对象关键字

  class:定义类
  interface : 定义接口
  extends:继承一个类
  implements: 实现一个接口
  super : 超级 (用于调用父类的成员)
  this: 当前类
  instanceof : 判断一个类的类型
  import : 导入一个类
  package : 定义一个类所在的包
  new : 创建对象

2.4 修饰符关键字

  abstract :修饰抽象类 、抽象方法
  final : 修饰常量
  native: 本地的 ,也用于修饰变量
  private :私有的
  protected : 受保护的
  public : 公共的
  static :静态的
  synchronized :修饰方法或代码块,用于线程安全的
  transient :瞬时状态
  volatile :瞬时状态

2.5 异常关键字

  try:试一试
  catch: 捕获异常
  finally: 最后执行的代码块 ( 多出的一个)
  throws :定义需要抛出的异常
  throw:抛出异常

2.6 其他关键字

  assert :测试中的断言
  strictfp : 其他
  enum:定义枚举

2.7 保留字

  const 、goto

标识符

1、定义

  在Java程序中,所有由程序员自己命名的元素统称为“标识符”

2、标识符命名规则

  标识符由数字、字母、下划线和$组成

  标识符不能以数字开头

  标识符区分大小写

  标识符不能是关键字

    注意事项:
  定义类名的标识符 :首字母必须大写 ,后面的单词的首字母大写,准寻 大驼峰命名法 (例如 XxxXxx ,UserInfo ,Student)
  定义方法标识符: 首字母尽量小写 ,后面的单词首字母大写,准寻   小驼峰命名法(例如 xxxXxxx , getUserName() )
  定义变量标识符: 单词全部小写 ( username)
  定义包名: 按模板分层级, 使用公司的域名 倒写,(例如 com.softeem.xxx 、com.j2008.xxx)
  定义项目名: 尽量使用英文 (Java项目 可以使用中文,JavaWeb项目一定使用英文)

标识符是否符合标识符是否符合
_$abcok$usernameok
AbcokUSERNAMEok
thisno#stuno
1usernamenoNULLok
nullnon1ok

六、Java的常量和变量

常量

1、定义

​ 在Java中,固定不变的数据量称为常量,常量也有内存,常量存放在被称为“常量池”的内存中

​ 面试题: 常量池的内存有哪些? (存放常量的内存有哪些 )

2、常量的分类

常量类型含义示例
整数常量所有的整数1,100,500 ,-20
小数常量所有的小数1.5,-3,4 ,3.1415926…
字符常量由单引号引起来,只能写一个字符,不能为’’‘a’ ,‘2’ ,’ ’
字符串常量由双引号引起来,可以写多个字符,可以为“”“hello” “\n” “\t”
布尔常量用于条件判断的 , 只有2个常量true 、false
空常量用于表示空对象null

​ 常量的修饰符 : final

  final  int  n = 100// 定义常量的语法 ,n的值只能为 100,不能再次改变 ,其中n就是常量

变量

1、定义

​ 在Java中用于保存一个数据的最小内存单元 称为 “变量” ,变量中每次只能存放一个值 ,变量的数值是可以被改变的 。

​ 在定义变量时,需要明确三要素: 数据类型 、 变量名(标识符)、变量值

2、变量语法:

​ 数据类型 变量名 = 初始值;

或者 数据类型 变量名1 ,变量名2 ;

// 定义一个变量 变量名是n ,数据类型是int ,初始值为100  
int  n = 100// 将原有变量名n中的值变更为 110  
n=110;

在这里插入图片描述

变量在赋值时,值必须满足与数据类型匹配,如果不匹配可能出现异常 或丢失精度

3、变量的分类 :

全局变量(global variables)

​ 在Java的类结构中,定义在类中,方法外面的变量, 它的作用范围: 整个类的方法中

public class Test1 {
    //定义全局变量 :定义在类里面,方法的外面
    int n = 100;

    //定义方法
    public void m(){
        // 输出一句话  "" 表示输出的内容    + 表示连接两个字符串
        System.out.println("n:"+n);
    }
    
    //定义方法
    public void s(){
        System.out.println("n:>>>"+n);
    }
}

局部变量(local varibable)

​ 一般声明在方法或代码块(条件语句块,循环语句块,静态语句块等) 中,它的作用范围只能在声明的区域中使用

 // 定义run方法
    public void run(){
        // 局部变量   只能在声明所在的方法中使用
        int  n=200;
        int  i =1;
        // 当类总存在局部变量和 全局变量同名时, 变量的使用遵循“就近原则”
        System.out.println("局部变量n:"+ n);   // 200
    }

    // 定义eat方法
    public void eat(){
       // System.out.println("i---"+i);
    }

调用方法的语法

 public static void  main(String [] args){
        // 如何调用 这个类的方法呢?  需要创建对象
        // 创建对象的语法:  类名 对象名 =  new  类名();
        //调用对象的方法语法:   对象名.方法名();
        Test1 obj = new Test1();
        obj.m();
        obj.s();
        obj.run();
        obj.eat();
    }

七、Java的数据类型

​ Java是一门强类型的编程语言,它不同于一些弱类型的语言(JavaScript ,php,python),Java在声明变量时必须显示的定义变量的数据类型,变量的类型一旦定义,则不能改变 。Java中数据类型分为两大类

1、基本数据类型 : 包括 整数,浮点,字符,布尔类型等

2、引用数据类型 :包括 类,数组,集合,接口等

1、基本数据类型

  • 整数型 ,默认值为0
数据类型关键字字节长度数值范围
字节型byte1个字节=8位-128~127
短整型short2个字节=16位-32768~32767
整型int(默认)4个字节=32位-2的31次方 ~2的31次方-1
长整型long8个字节=63位-2的63次方~2的63次方-1
  • 浮点型,默认值为0.0

    数据类型关键字字节长度范围
    单精度float4个字节1.4013E-45~3.4028E+38
    双精度double8个字节4.9E-324~1.7977E+308
  • 字符型 默认值 ‘ ’

    数据类型关键字字节长度范围
    字符型char2个字节0~65535

    一个字符能存放一个中文汉字

  • 布尔型 默认值 false

    数据类型关键字字节长度范围
    布尔类型boolean1个字节true、false

    注意:

           // 对于两个double类型/float类型的数计算, 由于计算机在计算式会缺失精度,计算的结果
            //不是预期的0.3,建议对于浮点数计算式,使用java.math.BigDecimal引用数据类型计算 
            double d1 =0.1;
            double d2 = 0.2;
            //结果 0.30000000000000004  
            double d3 = d1 + d2;
            System.out.println(d3);
    
            float f1 =0.2f;
            float f2 = 0.3f;
            float f3 = f1-f2;
    		// 结果  -0.1000000000001
            System.out.println(f3);
    

    对于字符类型中 "\ " 表示转移字符,不会占用内存

    \n :换行

    \t : 一个制表符tab

    \b: 表示退格

    \r: 表示enter

2、引用数据类型

 在Java中除了8个基本数据类型以外的数据类型,都是引用数据类型 ,常用的引用数据类型 包括  类、数组、 接口,String 等

 以类 举例

创建一个学生类    ,在一个类中会包含一些 变量(全局变量),还包含一些方法 

定义一个学生类,这个类就是引用数据类型 
  public class Student{
          // 定义的一个字符串变量  
         String stuname="张三";
         char  sex= '男';
         int age = 20// 定义一个方法 
         public void showInfo(){
             System.out.println(stuname);
             System.out.println(sex);
             System.out.println(age);
             
         }
      
  }

​ 如何使用这个学生类型(引用数据类型)

  public static void main(String [] args){
       // 定义学生类型  ,引用数据类型需要创建 引用的对象 
       // 引用数据类型   变量名  =   null 
      //  基本数据类型   变量名  = 初始值
      // 对于类的创建
      // 类名   对象名  =  new  类名(); 
      Student stu = new Student();
      // 对象名.方法名();
      stu.showInfo();
      
  }

引用数据类型与基本数据类型的区别?

1、创建方式不同 ,基本数据类直接通过定义变量的方式创建, 而引用数据类型需要new一个对象

​ 2、在JVM的内存分配不同, 基本数据类型的内存在栈内存中直接创建 , 而引用数据类型是在栈内存中定义引用的地址,实际的内存分布在堆内存中

​ 3、引用数据类型 可以调用它的方法, 而基本数据类型没有方法

​ 基本数据类型的分布:

在这里插入图片描述

八、Java的数据类型的转换

为什么需要进行数据类型转换

​ 在Java中数据进行计算时 ,必须要求计算的元素的数据类型一致,如果数据类型不一致需要进行转换

数据转换分类

1、自动类型转换

​ 转换规则: 范围小的类型向范围大的类型转换(从小到大转换)

​ byte -> short -> int-> long -> float -> double 其中boolean类型不参与转换

​ char ->int

​ 转换原理: 类型向上提升

2、强制类型转换

  • ​ 整数之间的强转
         // int 类型
          int i=1;
         // byte 类型
         byte  j=100;
         // 由于i和j 不同类型,不能直接计算,程序会自动将j的类型向上提升为 int
         // 所以i +j 之后依然是  int 类型
         // 此时 int 的内存 大于  byte 的内存,会出现数据溢出的情况 ,JVM不允许
         // byte s = i + j ;   // 等号两遍类型不匹配
         int y = i + j ;
         System.out.println(y);

         // 如果一定要使用byte赋值,还有一种办法,就是将两遍类型一致
         byte s  = (byte)(i + j) ;   // 两个数在相加时 会先自动提升为int 然后再相加

  • 整数与小数之间的强转
  // 整数和小数之前 也会丢失精度
        int n2 = 100;
        double n3 = 3.5;
        int n4 = (int)(n2 + n3);
        // 相加时先提升为 double ,然后相加后得到 double
        // double是8个字节 不能放在4个字节的长度中,
        // (这里好比double 为一桶水, int为一杯水,相当于将一桶水往一杯水中放)
        // 只能留住一本水的内容,所以  这里 如果强转成int 则小数会丢失,
        System.out.println(n4);
  • 小数和小数之间的强转
   // float  与 double 类型
        float f1 = 3.14f;
        double d2 = 3.5;
        double dd =  f1+d2;
        float  ff =  (float)(f1 + d2);

  • 丢失精度的情况
      // 强转时会丢失精度
        byte n=100;
        int  m = 30;
        byte mm = (byte)( n + m); // 因为最大值为127  再往后就是从最小值开始-128
        System.out.println(mm);

        // 整数和小数之间 也会丢失精度
        int n2 = 100;
        double n3 = 3.5;
        int n4 = (int)(n2 + n3);

九、Java的字符ASCII码表

字符int值
a97
A65
048
依次小写字母往后都是数字 ,例如 b为98,c为99…
A:65 B:66 C:67 0:48 1:49 2:50…

字符与数值的转换

// int 与 char类型的转换
        // 每一个char类型都有一个字符编码 这个编码参照 ASICC码表
        char c='a';
        // 自动提升 : 因为 char类型占2个字节, int占4个字节,自动char向上提升
        int n = c;
        System.out.println(n);
        // 先相加 再转换
        char c2 = (char)(n+2);  // 需要强转
        // 字符+字符  直接相加
        char c3 = 'a'+'b';
        System.out.println((int)c3);

十、运算符

​ 在Java中用于程序计算的操作符统称为运算符,运算符分为如下几类

1、算术运算符

运算符说明
+加号两遍是数值,可以运算,如果一边存在字符串,则当做连接符a+b
-两个数相减 , 减号也可以表示负数a-b -a
*两个数相乘, 其中*不能省略a*b
/两个数相除,必须保证数据类型一致,其中除数不能为0 ,否则出现算术异常a*b
%对一个数取余数a%b
++对一个数 自加1a++ 、++a
对一个数自减1a–、--a

// 总结 ++ – 的优先级高于 + ,-,*,、,%

  public static void main(String []  args){
        //  +  -  *  /  %    所有运算符在计算式必须保证 数据类型一致
        int num1 = 100;
        int num2 = 200;
        int sum = num1 + num2;
        int mul =  num1-num2;
        int num3 = -num2;   // -200
        System.out.println("两个数相加" +  sum); // + 表示连接符
        System.out.println("两个数相减" + mul);
        System.out.println( "num2: " + num2+ " ,num3:" + num3 );
        System.out.println("num1+num2="+ (num1+num2) );


        int sum2 = num1*num2;
        int sum3 = num1/num3;
        System.out.println(sum3);   // 控制台输出的快捷键 sout+ enter
       // System.out.println(num1/sum3); //算术异常:ArithmeticException: / by zero
        // 取模
        System.out.println( 10%2);//0
        System.out.println(1%5);// 1
        System.out.println(2%5);
    }
	public static void main(String[] args) {
// 自加  自减     ++  -- 只能对整数进行
        int i=1;
        i++ ;     // i = i + 1
        System.out.println("i:" + i);
        int j=1;
        ++j;    // j = j + 1
        System.out.println("j:" + j);

        int a =1;
        int sum = a++;  // 先将a的值赋值给sum  ,a再自加1
        int sum2 = ++a; // 先将a自加1 ,再将自加后的结果赋值给sum2
        System.out.println("sum:" + sum);
        System.out.println("sum2:" + sum2 );
        System.out.println("a:" +a);  // 3

        int sum3 = a++ + a++ ;
        System.out.println("sum3:"+sum3);
        System.out.println("a:"+a);

        // -- 操作  与++操作类似
        int b=1;
        int s1= b--;  // b = b -1     s1 的值为 先赋值再自减1   s1 = 1
        int s2 =  --b; // b = b -1   s2 的值为 先自减1 ,再赋值  s2 = -1
        System.out.println("b:"+b);
        System.out.println("s1:"+s1);
        System.out.println("s2:"+s2);

        // ++  --  综合
        int x=1;
        int y=2;
        int s3 = x++ + --x * (y-- + ++x);
        System.out.println("x:"+x);// 2
        System.out.println("y:"+y);// 1
        System.out.println("s3:"+s3);// 5
    }

2、比较运算符

​ 用于比较两个表达式之间的 结果 , 结果返回true 、false

​ 比较运算符不能单独作为 一行代码运行 ,必须接收结果或者输出

比较运算符说明案例
>比较左边的数是否大于右边的数,如果大于返回true,否则返回falsea>b ,3>5
<比较左边的数是否小于右边的数,如果大于返回true,否则返回falsea<b
>=比较左边的数是否大于等于右边的数,如果大于返回true, 否则返回false1>=2
<=比较左边的数是否小于等于右边的数,如果大于返回true ,否则返回false1<=2
==比较==两遍是否相等,如果相等则返回true ,否则返回false ,对于基本数据类型比较数值是否相等, 对于引用数据类型比较 它们的地址是否相等 (比较地址就是比较是否同一块内存)1==2
!=与== 相反, 比较左边和右边的数是否不相等 。如果不相等返回true ,如果相等返回false1!=2

注意: 不能连写 例如 1<3<5

 public static void main(String[] args) {
          // 比较运算符
        int a =1;
        int b =2;
       // a>b;  // 不能单独比较 必须接受结果或输出
        System.out.println(a>b);  //false
        System.out.println(a<b);  //true
        System.out.println(a==b); //false
        System.out.println(a!=b); //true
        // 增加逻辑判断
        // 如果 if 后面的条件成立,  则执行if里面的语句 ,如果不成立 则只需else语句
        if(a>b){
            System.out.println("a>b成立");
        }else{
            System.out.println("不成立");
        }

    }

3、赋值运算符

​ 将表达式 的结果 赋值给一个变量,只能赋值给变量 不能赋值给常量

​ 例如: a = 3

赋值运算符说明案例
=将=右边的结果 赋值给左边的变量int a = 2 ,将2赋值给变量a
+=计算后的赋值,将+=右边的结果赋值给左边的变量a+=2 等价于 a = a +2
-=计算后赋值,将-=右边的结果赋值给左边的变量a-=b 等价于 a = a-b
*=同上, 将右边的结果赋值给左边的变量a*=b 等价于 a = a * b
/=同上,将右边的结果赋值给左边的变量a /=b 等价于 a = a/b 其中b!=0
%=同上,将右边的结果赋值给左边的变量a %=b 等于 a= a%b 其中b!=0
       // 赋值运算符 = +=  -= *=  /=  %=
        int a=2;
        a+=2;
        System.out.println(a);// 4
        a-=3;
        System.out.println(a);// 1
        a*=2;
        System.out.println(a); // 2
        a/=4;
        System.out.println(a);// 0

        a+=a-=3; // a+=(a-=3) -》 a=a +(a=a-3 )
        System.out.println("a="+a);

        int x=2;
        x+=x-=x*=x++;
        //x = x +(x = x -( x= x *(x++) ))
        // x = 2 + ( x = 2 - (x = 2 * 2))
        // x =  2 + ( 2 - 4)
        // x = 0
        System.out.println("x="+x);

        //赋值运算符的优先级最低,  从右往左计算
        int y=2;
        y*=y+=y; // 也是从右往左计算
        // y = y * (y = y + y);
        // y = 2 * (2+2)
        // y =8;
        System.out.println("y="+y);

4、逻辑运算符

在Java中用于两个或两个以上的表达式 取逻辑判断的结果 ,通常需要使用逻辑运算符

逻辑运算符说明案例
&逻辑与 ,左右两边可以连接表达式 ,两个表达式都成立时,整个结果为truetrue&true、 false&true false&false 、 true&false
|逻辑或,左右两边的表达式,任意一个成立,整个结果为truetrue|true false|true true|false false|false
!逻辑非 对表达式取反!false !true
&&短路与 , 与&类似 ,短路 特点: 符号左边为false,右边不再运算true&&true true&&false …
||段落或, 与| 类似,段落忒点: 符号左边为true ,右边不再运算false||true false||false
  // 逻辑运算符
        System.out.println(true & true); // true
        System.out.println(true & false);// false
        System.out.println(false & true); // false
        System.out.println(false & false);// false
        // true&true
        System.out.println(1>0 & 3>1);
        System.out.println(1>0 && 3>1);
        // | 或
        System.out.println(true | true); //true
        System.out.println(true | false);// true
        System.out.println(false | true); // true
        System.out.println(false | false);// false
        // || 短路或
        System.out.println(true || false) ;// true

总结: &与&&的区别 |与||的区别?

回答 1、& 对于符号两边的表达式都会执行 && 符号左边为false,则右边不执行

​ | 对于符号两边的表达式都会执行, || 符号左边为true,则右边不执行

​ 2、 & 、| 两边可以直接写数字, 按位计算 ,短路符号 不能直接运算数字

  int a=1;
        int b=2;
     //   System.out.println(a>b && b++>0);  // 符号右边的不运算
     //   System.out.println("b:"+b);

        System.out.println(a>b & b++>0); // 符号两边都运行
        System.out.println("b:"+b);

        // || 与 | 的区别
      //  System.out.println(a>=1 ||  a++<0); // a++ 不执行
      //  System.out.println("a:"+a);

        System.out.println(a>=1 | a++<0 );  // a++ 会执行
        System.out.println("再次输出a :" + a);

十进制转二进制

十进制转换二进制
11*2的0次方1
21*2的1次方10
41*2的2次方100
对于2的倍数 ,1*2的n次方 转成二进制位1后面n个0
161*2的4次方10000

对于任意十进制转二进制,先查找比它小的且离它最近的2的倍数 ,然后差值再计算二进制 ,(使用拆分法)

例如 28 = 16+8+4 = 11100

​ 34 = 32+2 = 100010

​ 96 = 64+32 = 1100000

二进制转十进制

公式: 从个位开始,每个位上的数 乘以2的n次方 累加之和

100100 = 1*2的2次方+1 * 2的5次方 = 36

5、位运算符

移位运算符说明案例
<<左移: 将一个数转成二进制之后,整体往左移动n位 ,扩大倍数 ,一个数向左移动n位,结果为这个数乘以2的n次方3<<2 = 3*2的2次方
>>右移:将一个数转成二进制后,整体往右移动n位,缩小倍数,一个数向右移动n位,结果为这个数除以2的n次方(除不尽单独考虑)8>>2 = 8/2的2次方
> > >无符号的右移 ,不考虑符号位,将这个数整体王右移动n位。
^n异或
~n数值取反
   //正整数的移位  <<   >>   >>>
        System.out.println(3<<2);  // 12
        System.out.println(7<<3); // 7*8=56

        // 对于正数的无符号右移和 右移 结果一样
        System.out.println(16>>2);// 4
        System.out.println(18>>2);// 4

        System.out.println(5^9);//12


        System.out.println(3>>2);
        System.out.println(3>>>2);

        System.out.println(~5);

// 负数的移位
        // 负数的左移位还是为负数
        System.out.println(-4<<2); // -4*2的2次方 =
        /**
         *   -4的原码: 1 0000... 00000100
         *          *   -4的反码: 1 1111... 11111011
         *          *   -4的补码: 1 1111... 11111100
         *          *   开始移位   2
         *             1 1111... 11110000
         *    最后结果 = 取反+1
         *             1 0000... 00001111  + 1
         *  :
         *             1 0000... 00010000  =-16
         */

        // 补码 = 反码+1
        //负数是对于补码 进行移位   -4/2 =-2
        System.out.println(-4>>1);

        // -16无符号右移2位
        System.out.println(-16>>>2);   //1073741820   
        // -16 的补码算出来
        /**
         * 原码  10000.. 0010000
         * 反码  11111.. 1101111
         * 补码  11111.. 1110000
         *      00111.. 1111100  由于不考虑符号,移动后高位全部补0 变成正数
         *      正数原码和补码一致 这个数即为所得
         *      1073741820
         */

6、三目运算符

​ 表达式 ? 结果1 : 结果2

当?前面成立时, 整个表达式输出 结果1 ,如果?前面不成立,则输出结果2

  // 生成100以内的随机数
        int n = (int)(Math.random()*100);
        System.out.println("n:"+n);
        System.out.println( n%2==0 ?"这个数是偶数":"这个数是奇数");

十一、表达式、语句块

表达式 : 通过运算符 和 操作数组成的 元素 , 表达式不能单独写在程序中,需要接受结果或输出。

表达式中可能同时存在多个操作符 ,此时需要考虑操作符的优先级问题

在这里插入图片描述

注意 : 这里的() 表示 方法的括号 ,点号表示小数点 ,[] 表示数组的下标

2-5 : 用于得到结果值

6-12:用于运算得到ture、false

13,14:是赋值 。赋值的优先级最低

语句块:

在一个方法中,可以使用{} 表示一个语句块 , 也可以放在方法的外面 ,类的里面,称为独立代码块

  public static void main(String[] args) {
         //定义一个方法中的代码块
        {
            // 局部变量只能使用在它所在的区域
            int a=1;
            a++;
            System.out.println("这是一个方法里面的代码块 "+a);
        }
       // a++;
        if(true){
            System.out.println("这是一个if代码块");
        }
    }

十二、流程控制

1、定义

​ 在一个Java程序中,各条语句的执行对程序的结果有直接影响,也就是说 各个语句的执行顺序对程序的结果有直接影响。

​ 在程序中 ,可能出现不同的执行顺序,必须 自上而下顺序执行,或者 条件判断的顺序或者循环执行的顺序。

2、分类

​ 顺序执行

​ 条件分支

​ 循环执行

3、顺序执行

 //顺序执行, 从上而下执行 
public static void main(String [] args){
      System.out.println(1);
       System.out.println(2);
        System.out.println(3);
 }

4、条件分支

1、if条件分支

​ 语法:

if(条件){
    语句块
}
其他代码

解释: 如果条件成立 ,则执行语句块 ,如果条件不成立,则不执行语句块

在这里插入图片描述

   // 生成一个100以内的随机数   判断它是否为偶数
        int n = (int)( Math.random()*100);
        if(n%2 == 0){
            System.out.println("这是数是偶数");
        }
        System.out.println("程序结束");

2、if…else条件分支

语法:

  if(条件){
       语句块1
  }else{
      语句块2
  }

解释: 如果条件成立, 则执行语句块1 ,如果条件不能力 ,则执行语句块2

在这里插入图片描述

  int n = (int)(Math.random()*100);
        // n<50 需要买西瓜   >50 需要买葡萄
        if(n<50){
            System.out.println("买了一个大西瓜");
        }else{
            System.out.println("买了一串葡萄");
        }
        System.out.println("n->"+n);
        System.out.println(" 嗯,程序猿的女朋友很高兴,至少买了水果");

3、if…else if … else 多条件分支

语法:

 if(条件1){
     语句块1
 }else if(条件2){
     语句块2
 }else if(条件3){
     语句块3
 }
 ...
 else{
     语句块4
 }

解释: 从条件1开始依次判断,如果条件1 成立,则执行语句块1 ,其他条件不再执行,如果条件2成立,则执行语句块2,其他条件不再执行。。。 依次类推如果条件都不成立,则执行else语句块。 ,最终只执行其中某一个语句块,
在这里插入图片描述

   // 随机生成90以内的年龄  整数
        int n = (int)(Math.random()*90);
        if(n<18){
            System.out.println("未成年");
        }else if( n<30){
            System.out.println("青年");
        }else if( n<50){
            System.out.println("中年");
        }else if( n<70){
            System.out.println("老年");
        }else{
            System.out.println("晚年");
        }
        System.out.println("n->"+n); 

嵌套条件判断

语法: 以上3种格式 ,可以同时使用,在一个if语句再次嵌套if 语句

        // 接收控制台输入 ,  判断 输入的数 是否能被3整除
          // 如果能被3整除,输出这个数除以3的结果,并判断结果能被7整除
           // 如果不能被3整除,判断是否为偶数

        Scanner sc = new Scanner(System.in);
        // 接收控制台输入的整数
        int n = sc.nextInt();
        if(n%3 == 0 ){
            System.out.println("这个数能被3整除");
            // 在if语句中继续判断, 就是嵌套条件判断 ,需要往后缩进
            int result= n/3;
         	if(result%7 == 0){
                System.out.println("这个结果能被7整除");
            }else{
                System.out.println("这个结果不能被7整除");
            }
        }else{
            System.out.println("这个数不能被3整除");
            if(n%2 ==0){
                System.out.println("这个数能2整除");
            }else{
                System.out.println("这个数不能被2整除");
            }
        }

注意 : 嵌套条件时 为了增强代码的可读性,将条件语句块的分支 往后缩进 ,{}作为一个整体

​ 条件语句块中如果只有一个输出语句, 可以省略{}

4、选择条件判断

语法:

switch(表达式){
    case 常量值1:
       语句块1;
      break;    // 语句块接收的标记
    case 常量值2:
       语句块2break;
      ...
    default: 
       语句块3break}

注意: switch 的表达式判断 只能等值比较 ,其中case的常量值 类型位: 整数型(byte short int long ),字符型,字符串型,枚举型

  byte n = (byte)(Math.random()*7+1);
        switch (n){
            case 1 :
                System.out.println("星期一");
                break;
            case 2:
                System.out.println("星期二");
                break;
            case 3:
                System.out.println("星期三");
                break;
            case 4:
                System.out.println("星期四");
                break;
            case 5:
                System.out.println("星期五");
                break;
            case 6 :
                System.out.println("星期六");
                break;
            default :
                System.out.println("星期天");
                break;
        }

case穿透问题

在switch中,如果case后面不写break,将会出现穿透现象,也就是说不会执行下一个case的判断条件直接往后运行,直到遇到break,或整体switch结束

练习

* 1、使用switch制作一个简单的计算器:
* 让用户输入计算数字1和运算符 及计算数字2,然后程序帮他计算出结果。
*

十三、循环

1、循环定义

  在Java程序中,重复的执行某一段代码 这个过程称之为循环, 为了避免出现死循环,循环分为四部分

​ 1、初始条件

​ 2、循环的判断条件 ,条件为true ,则进入循环体

​ 3、循环体

​ 4、迭代变量

while循环

语法:

  初始条件
  while(判断条件){
      循环体
      迭代部分 (为了改变循环的判断条件)
  }

在这里插入图片描述

​ 计算1到100的累计之和 1+2+3+4…+100=?

初始值 n=  1
条件:  100以内的 数可以一直累加(一直循环)
迭代: n++ 
 //计算 1到100的累加之和
        int sum=0;
        int  n=1;
        while(n<=100){
            sum+=n;
            n++;
        }
        System.out.println("n:"+n);
        System.out.println("sum:"+sum);

do…while循环

语法:

 初始值1
 do{
     循环体2
     迭代部分3
 }while(返回boolean类型的表达式4);

执行顺序: 123-》423 -》423-》423 .。。4 直到条件4为false 则退出循环。

​ 先执行初始值 1,循环体2 ,迭代3

​ 再判断条件4是否成立,成立,继续执行2,3

再判断条件4是否成立,成立,继续执行2,3

​ 判断条件4是否成立,不成立,退出

        int i=0;
          do{
              System.out.println("i--"+i);
              i++;
          }while(i<10);

        System.out.println("i===="+i);
        /**
         * 第一遍: 输出 0   i=1
         * 第二遍: 判断 1<10  成立  输出1   i=2
         * 第三遍: 判断 2<10  成立  输出2   i=3
         * .。。
         * 第九遍: 判断8<10  成立  输出8   i=9
         * 第十遍:判断 9<10  成立 输出9    i=10
         * 第十一遍: 判断 10<10 不成立。退出
         *
         *
         */

问题: while循环与do while循环的区别?

1、while循环先判断条件是否成立,再执行循环体,do while循环 先执行一遍循环体再判断条件。

break : 退出循环

for循环

​ for循环的升级版就是 foreach循环

​ for循环通常在明确循环次数的情况下使用, 它也分为四部分

语法:

 for(初始值1 ; 循环判断条件2 ; 迭代部分3 ){
     循环体4
 }
 或者
  
 初始值
 for( ; 循环判断条件 ; ){
     
     循环体
     
     迭代部分
 }
 // 他们的区别是 初始值定义在for的外边,就可以在循环外边使用

循环执行流程:

​ 1243-》243-》243-》243-》。。。》2 为false 循环退出

例如 :循环输出5遍hello world

  // i 表示计数器,从1开始 到5结束
 for(int i=1 ;i<=5 ; i++){
      System.out.println("hello world");
  }

循环的执行顺序:

第一遍: i=1 1<5 成立 输出“hello world ” i++ i=2

第二遍 :2<=5 成立 输出“hello world” i=3

第三遍 : 3<=5 成立 输出”hello world“ i=4

第四遍: 4<=5成立 输出”hello world“ i=5

第五遍: 5<=5 成立 输出”hello world“ i=6

第六遍: 6<=5 不成立,退出

i=6

使用for循环计算1-的阶乘

 // 使用for循环 计算 10的阶乘
        for(int i=1;i<=10;i++){
            sum*=i;
        }

for循环部分可以省略

        // 死循环
        for(;;){
            System.out.println("hello");
        }
        for(;true;){
           System.out.println("hello world");
        }
  // 迭代部分的代码 可以 放入循环体中
  int i=0;
  for( ; i<10; ){
  System.out.println("第"+i+"次输出");
  i++;
  }

十四、关键字 break、continue 、return的区别

1、break : 用于在switch。。case中放置语句块穿透,

​ 用于跳出循环

 // 从1-100  遇到7的倍数 break
          for(int i=1;i<100;i++){
              // i==7 跳出循环
              if(i%7 == 0 ){
                  break;
              }
              System.out.println(i);
          }

2、continue: 跳出本次循环,继续下一次循环

 for(int i=0;i<100;i++){
              if(i%7 == 0 ){
                  continue;  // 跳出本次循环,继续下一次循环
              }
              System.out.println("i---"+i);
    }

3、return : 返回 本次方法

​ 用法1 : 如果return放在循环中, 会跳出循环,且不会只想循环外面的语句 ,

​ 用法2: 作为方法的返回值

​ 用法3 : 无论方法是否有返回值 ,可以在条件判断的位置 直接 return ,

return和break在循环语句块是,break只是结束循环语句块,对于循环外面的代码会执行,而return是结束当前所在方法的剩下的语句块。

  public static void main(String[] args) {
           for(int i = 1;i<100;i++) {
               if (i == 50) {
                   return;
               }
               System.out.println("i----"+i);
           }

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

    }

   public void method1(){
         // return 还可以在条件判断的位置 直接返回方法
        int n = (int)(Math.random()*10);
        if(n%2 ==0){
            return  ; // 方法的后面就不再运行
        }
        System.out.println("方法的其他代码块");
        
    }

结论:只要执行return,那么它 后面的代码都不执行。

    public int add(){
        return 0;
       
    }

return作为方法返回值的关键字 ,

十五、嵌套循环 以及案例

嵌套循环: 在一个循环语句中,还包含了另一个循环。例如 在一个for循环中还有一个for循环 ,

它的总的循环次数 = 外循环的次数* 内循环的次数

语法:

  for(){    // 外层循环

		for(){   // 内层循环

		}

	}

执行顺序: 外层循环 循环一次 ,内层循环循环完整的一遍

*	*	*	*	*
 打印直角三角形
* 
* * 
* * * 
* * * * 
* * * * * 
外循环控制打印几行, 内循环控制打印即可* 
        *
      * * *
    * * * * *
  * * * * * * *
* * * * * * * * *
  * * * * * * *
    * * * * *
      * * *
        *

思路 : 考虑一行打多少个空格 多少个*

一共5 行 空格的个数(5-i) *的个数 (2 * i - 1)

i=1 4 1

i=2 3 3

i=3 2 5

i=4 1 7

i=5 0 9

 System.out.println("打印正三角形");
            for(int i=1;i<=5;i++){
                // 先打印空格
                for(int k=0;k<5-i;k++){
                    System.out.print("  ");
                }
                // 再打印*
                for(int j=0;j<2*i-1;j++){
                    System.out.print("* ");
                }
                // 换行
                System.out.println();
            }

九九乘法表

1*1=1
1*2=2 2*2=4
1*3=3 2*3=6 3*3=9
1*4=4 2*4=8 3*4=12 4*4=16
....
1*9=9 2*9=18 ......                   9*9=81

十六、数组的概念以及数组案例

1、容器的概念

​ 用于存储数据的一块内存称为容器,生活中有很多容器,例如 水杯,衣柜,书包,有一定的空间可以存放“东西”

​ 存放在容器中的数据 称为“元素”

2、为什么会存在数组呢?

​ 假如现在要存储全班同学的成绩 , 全班40人,按照定义变量的思维,需要定义40个double类型的变量,每次从40个变量中找某一个变量,操作很麻烦, Java中可以定义一个数据类型存放40个人的成绩 , 这个类型就是数组类型。

数组定义: 它是相同数据类型的有序集合

3、 数组特点

  • 数组的长度固定(数组的长度一旦声明,就不能改变)
  • 数组中存储的元素数据类型必须相同
  • 数组的元素 通过下标访问,且下标默认从0 开始
  • 数组类型属于引用数据类型, 数组的元素类型 既可以是基本数据类型,也可以是引用数据类型。

4、创建数组

方式一:

数组存储的数据类型 [] 数组名  = new 数组存储的数据类型[长度]

详解:

数组存储的数据类型 :创建数组容器中可以存储什么数据类型 (基本数据类型 ,引用数据类型)

[] : 表示数组

数组名: 给数组起给名字,遵循标识符规则

new : 创建数组的关键 字

[长度] : 数组的长度 , 这个长度定义后不可改变

例如

int [] arr = new int[3];

new出来的空间在堆内存中,数组是引用数据类型 ,存在内存地址

​ 内存解析: 在堆内存中开辟一段连续的3个长度的int类型的内存空间 ,并由arr变量指向这块内存的地址 (换句话说 arr输出的就是 这个内存的地址)

在这里插入图片描述

方式二:

 数据类型 [] 数组名 = new 数据类型[]{元素1,元素2,元素3...}

​ 这里的数组长度根据元素的个数自动分配大小

int [] arr = new int[]{90,88,78,92};
或者
int arr [] = new int[]{90,88,78,92}

方式三:

  数据类型 [] 数组名  = {元素1,元素2,元素3...};

注意: 这里的元素类型必须满足 数组的元素数据类型

 char [] arr = {'a','b','c'};
 或者
 char arr [] = {'a','b','c'};

5、数组的访问

​ 数组的访问通过索引访问

索引(下标): 每一个数组的元素都有一个编号,这个编号从0开始 , 这个编号称为数组的索引,通过数据名[索引] 访问到数组的原始

例如: 访问数组的第二个元素: 数组名[1]

数组的长度: 数组的长度 声明已固定 ,访问数组的长度 : 数组名.length

​ 数组的最大索引= 数组的长度 -1

数组元素的赋值 :通过索引可以给元素赋值 数组名[索引] = 值

​ 将数据 赋值给 指定索引的 元素

十七、方法

方法的概念

​ 将一个功能抽取出来,放在类中的大括号中, 形成一个独立的功能 , 当需要使用该功能时,则调用它, 这样可以增强代码的复用性(重复利用) ,并解决代码的冗余现象。

方法的语法:

​ [访问修饰符] 返回值类型 方法名( [参数类型 参数名1 , 参数类型 参数名2 …] ){

​ 方法体

​ }

​ 详解:

​ 访问修饰符: 用于修饰这个方法的调用范文 目前默认 public static

​ 返回值类型: 无返回值就写void 或者 方法执行后返回的结果的数据类型 ,方法执行完之后会将结果返回

​ 方法名 : 给方法取名, 准寻标识符的规则

​ 参数类型 、参数名: 它们是同时定义 ,方法在执行时未知的数据,需要在调用方法时进行传递值。 参数类型表示 这个参数的数据类型 ,参数名就是一个参数名称 这个参数名可以在方法体中使用。

​ 方法体: 这个方法具有的功能(代码块)

    定义一个方法
    public static int add(int num1 , int num2){
    	  
    	  return num1+num2;
    
    } 

​ 根据方法的参数 不同,返回值不同,可以将方法分为四类:

1、无参无返回值方法

语法:

   public  static  void 方法名(){
       方法体 
   }

2、无参有返回值方法

​ 语法:

 public static 返回值类型  方法名 (){
     方法体
 }

在这里插入图片描述

结果在n中

3、有参无返回值方法

​ 语法:

public  static void 方法名(参数列表 ){
    方法体
}

情况一、 当参数中是基本数据类型 ,将实参的值赋值给 形参

在这里插入图片描述

public  static void add(int num){
    num++;	
    System.out.println("方法中 num 的值:"+num);

}

调用方法: num的改变不会改变n 的值 ,因为是两个独立的内存

int n =10;
System.out.println(n);
add(n);
System.out.println("方法外面 实参的值:"+n);

结果输出:

​ 方法中 num 的值: 11

​ 方法外面 实参的值:10

情况二: 当参数的数据类型是引用数据类型

例如 数组 、类

 // 方法的比对   参数是数组
    public static  void add(int [] arr){
        arr[0]++;
        System.out.println("方法中 arr[0]="+arr[0]);
    }

调用:

       int [] array ={10,20};
        add(array);// 会调用 参数是数组的add方法
        System.out.println("方法外面的 arr[0]="+ array[0]);

结果:

​ 方法中 arr[0]=11
​ 方法外面的 arr[0]=11

在这里插入图片描述

类的调用:在这里插入图片描述

  public static void addAge(Student student){
        // 将学生的年龄 获取 自加
       int age =  student.age;
       age++;
       //  在重新赋值
       student.age=age;

    }
    

调用

 Student student = new Student(); // age默认为 18
        System.out.println("方法调用之前: "+student.age); // 18
         addAge(student); // 传的地址
        System.out.println("方法外面 学生的年龄:"+student.age);

结果输出: 方法调用之前: 18

​ 方法外面 学生的年龄:19

  /**
         *   以上方法调用 的结论: 对于参数为引用数据类型,方法外面和方法内部公用同一块内存,
         *       也就是说  参数再传递时,将引用类型的地址赋值给 方法的形参
         *                      对于基本数据类型 ,方法的参数将 值的副本赋值给形参,这样形参的改变
         *                      不会影响实参的值
         *       原因: 引用数据类型在参数传递时 是地址  (JVM对于堆内存的大小不可控)
         *             基本数据类型在参数传递时 是值
         *
         */

情况三、 当参数是String类型时 ,String是引用数据类型 ,但是在参数传递时,是与基本类型一样

 public static void main(String[] args) {
    // 参数是字符串
        String uname ="张三";
        sayHello(uname);
        System.out.println("我最后对"+uname+"sayHello");
    }
    public static void sayHello(String name){
        name="李四";
        System.out.println("我对"+name+"sayHello");
    }

结果:

我对李四sayHello
我最后对张三sayHello

4、有参有返回值方法

语法:

  public  static 返回值类型 方法名(参数列表){
      方法体
  }

例如 :

    public static String sayHello(String name){
        name="李四";
        System.out.println("我对"+name+"sayHello");
        return name ;
    }

调用

 public static void main(String[] args) {		
		// 参数是字符串
        String uname ="张三";
        // 将返回值接收  ,这是 uname 才被改变 ,如果不接受,则不改变
        uname = sayHello(uname);
        System.out.println("我最后对"+uname+"sayHello");
 }

结果:

我对李四sayHello
我最后对李四sayHello

十八、动态数组

1、数组的定义

​ 用于存储相同数据类型的一组连续的存储空间

2、数组的特点:

​ 数组的长度一旦定义,则不可改变

​ 访问数组的元素需要通过下标(索引)访问,下标从0开始

​ 数组是引用数据内存,内存分布在堆内存中,数组的变量存储的内存地址

3、动态数组:

​ 由于数组的长度定义后不能改变,所谓“动态数组”是可以增加数组的长度, 所以Java实现动态数组是改变数组变量指向不同的内存的地址。 本质并没有将数组的长度改变。

​ 动态数组的本质: 将内存空间的改变, 以及指向数组内存的地址改变

​ 操作1 : 给数组 添加新元素 ,可添加在最后面 ,也可添加到指定位置

  /**
     *  添加元素 (添加到末尾)
     */
    public static int [] addEle(int [] array ,int num){
        // 目标数组    添加的原始
        //int [] array = {10,9,3,2,1};

        // 1、创建临时数组的变量
        int [] tempArray = new int[array.length+1];
        //2、将目标数组的元素 copy到 临时数组的内存中
        for(int i = 0 ;i<array.length;i++){
            tempArray[i]= array[i];
        }
        // 3、将添加的元素放入临时数组中
        tempArray[tempArray.length-1] = num;
        // 4、将目标数组的地址 指向 临时数组的地址
        array= tempArray;  // 由于tempArray 是局部变量, 方法执行完内存自动回收 ,
                            // 如果不返回  没有地址指向tempArray的内存
                            // 如果返回并接收,说明这块内存仍然有用。
        return array;

    }

  /**
     * 将元素num 添加到指定index的位置
     * @param arr
     * @param num
     * @param index
     * @return
     */
    public static int [] addEle(int [] arr , int num ,int index){
          // 1、创建临时数组的大小
           int [] tempArray = new int[arr.length+1];
           //2、遍历arr
           for(int i = 0 ; i<=arr.length;i++){
               //  如果i<index
               if(i<index){
                   tempArray[i] = arr[i];
               }else if(i==index){ // 2
                   tempArray[i] = num;
               }else{ // i > index
                   //  i=3   arr[i-1]   10  9  3  2  1     -> 10  9  5  3  0  0
                   // i =4   array[i-1]  2->    10  9  5 3  2 0
                   // i=5     array[4]   1  ->   10 9  5  3  2 1
                    tempArray[i] = arr[i-1];
               }
           }
           // 赋值
           arr = tempArray;
           return arr;

    }

操作2: 删除元素 ,删除指定下标的元素

  /**
     *  删除指定下标的元素
     * @param arr 目标数组
     * @param index  删除的下标
     * @return 删除之后的数组
     */
    public static int [] removeEle(int [] arr, int index){

         // 1、创建一个临时数组 用于存放删除后的原始
        int [] tempArray = new int [arr.length-1];
        //  2、遍历目标数组
        for(int i = 0 ;i<arr.length;i++){
            if(i<index){
                tempArray[i] = arr[i];
            }else if(i==index){
                continue;
            }else {//i>index
                tempArray[i-1] = arr[i];
            }
        }
        // 3 将目标数组的地址变换成 新数组的地址
        arr=tempArray;
        return arr;

    }

十九、数组的排序

​ 排序: 将一组数列(无序的)按照从小到大或者从大到小的顺序排列。

(img-MOygkjYf-1602428824057)(assets/1601365263369.png)]

1、冒泡排序

​ 从第一个数开始, 与它相邻的数比较 ,较大(或较小)的数放在后面,最终比较一轮之后,得出最大(或最小)的数放在最后

​ 比较思路:

冒泡排序的规则
*    1、从第一个数开始,将这个数 与它相邻的数比较 ,如果 这个数大于它相邻的数
*      则两个数交换位置
*       i=0   相邻i=1
*    2、依次 从第二个数开始,再将这个数与它相邻的数比较,如果第二个数大于相邻的数
*       则继续交换
*       依次类推, 到倒数第二个截止 ,直到将最大的数放在最后面
*     3、重复以上1,2步骤
/
    
  public static void main(String[] args) {
    int [] array = {5,4,3,2,1};

        //用于交换的临时变量
        int temp=0;

        for(int j =0;j<array.length-1;j++) {
            for (int i = 0; i < array.length -j-1; i++) {
                // 相邻的数比较
                if (array[i] > array[i + 1]) {
                    temp = array[i];
                    array[i] = array[i + 1];
                    array[i + 1] = temp;
                }
            }
            System.out.println("比较一轮之后:" + Arrays.toString(array));
        }
}
    

2、选择排序

 /**
     *  选择排序: 从一堆数中选择一个最小数 放在第一个位置,再从一堆数中选择一个
     *          最小数放在第二个位置, 依次 将一堆数的最小数按顺序排放。
     *    步骤: 1、假设第一个数是最小数,需要定义最小数的下标minIndex=0
     *             将这个数与后面的每一个数比较,找到最小数的下标即可
     *          2、将第一个数与最小数的下标交换 ,得出最小数在第一位。
     *          3、 依次类推, 将已比较的数 忽略,继续从生下的元素中找足最小数,放入已比较的数的下一位
     *              直到整个数列比较结束
     * @param args
     */
    public static void main(String[] args) {
            int [] array = {3,2,1,5,7,4};
            for(int j=0;j<array.length-1;j++) {
                // 假设第一个数是最小数
                int minIndex = j;
                // 为什么i =j+1  因为初始值要略过已比较的下标
                for (int i = 1+j; i < array.length; i++) {
                    if (array[minIndex] > array[i]) {
                        minIndex = i;
                    }
                }
                
                // 将这个最小数放在 第一位
                int temp = 0;
                temp = array[j];
                array[j] = array[minIndex];
                array[minIndex] = temp;

                System.out.println("----第一次完成后:" + Arrays.toString(array));
            }

        System.out.println("最后的排序:"+Arrays.toString(array));
    }
}

3、插入排序

/**
     * 插入排序
     *    1、从第一个元素开始,假设第一个元素是已排好序的
     *    2、从下一个元素开始,依次比较它前面的所有元素(从后向前扫描)
     *    3、 如果这个元素 小于它前面的元素 则两两交换 ,
     *         如果这个元素 大于它前面的元素,则不交换
     *    4、依次重复2,3步骤  ,直到将所有数 比较完成
     *   5,4,3,2,1
     *
     *   4 5 3  2  1   i从1开始
     *
     *   4 3  5 2  1    i从2开始
     *   3 4  5 2 1
     *
     *   3 4 2 5 1    i从3开始
     *   3 2 4 5 1
     *   2 3 4 5 1
     *
     *   2 3 4 1 5   i从4开始
     *   2 3 1 4 5
     *   2 1 3 4 5
     *   1 2 3 4 5
     * @param args
     */
    public static void main(String[] args) {
        int [] array = {5,4,3,2,1};
        // 外层循环循环  每一个数的比较次数
        for(int j=0;j<array.length-1;j++) {
            int temp = 0;
            for (int i = 1+j; i > 0; i--) {
                if (array[i] < array[i - 1]) {
                    temp = array[i];
                    array[i] = array[i - 1];
                    array[i - 1] = temp;
                }
            }
            System.out.println("每一次完成后的结果:"+ Arrays.toString(array));
        }
        System.out.println("最后一次完成后的结果:"+Arrays.toString(array));

    }

二十、二维数组以及多维数组

1、二维数组定义:

​ 在一维数组中定义每一个元素也是一个数组元素,这样的数组称为“二维数组”

​ 多维数组就是在一维数组上再次定义二维数组或三维数组等。

​ 一维数组定义 int [] array = { 1, 2 , 3}

 //定义三个长度的二维数组,其数组的每一个元素是一个一维数组 
 int [][] arrays = {{}{}{}} ;  
 或者
 int [][] arrays = new int [3][2];  // 左边的 [] 中表示二维数组的长度  ,其中2可以省略,3 不能省略 
  // 注意: 等号左边有几个 [] 就表示几维
 

在这里插入图片描述

  //  1、定义二维数组
        int [][] array;
        //  定义时给二维数组赋值  3个长度的二维数组  里面的一维数组的长度不一定相等
        int [][] array2 = {{1,2,3},{4,5},{7,8}};
        //  定义时只指定大小 不给定初始值
        int [][] array3 = new int[3][]; // 等价 {{},{},{}}
       // array3[0][0]=1; // 赋值时 空指针异常 ,因为里面的一维数组是空的

        //定义一个3个长度的二维数组,里面的元素长度是2
         int array4 [][] = new int[3][2];
         //给元素赋值
        array4[0][0] =1;

  // 输出二维数组中的所有元素
        for(int i=0;i<array4.length;i++){
           // System.out.println(array4[i]);
            for(int j=0;j<array4[i].length;j++){
                System.out.println(array4[i][j]);
            }
        }

二十一、二维数组的应用

1、定义5*5的矩阵,计算最大值最小值

2、五子棋游戏

二十二、面向对象语言编程

​ Java是一门面向对象的编程语言(OOP),万物皆对象

面向对象初步认识,在大多数编程语言中根据解决问题的思维方式不同分为两种编程语言

1、面向过程编程

2、面向对象编程

面向过程面向对象
区别事物比较简单,可以使用线性思维解决,具体每一个实现步骤清晰可见事物比较复杂使用简单的线性思维无法解决,存在对象与对象之间的引用
共同点1、都是为了解决实际问题的一种方式 2、当解决复杂问题时,面向对象是从宏观角度把握问题的整体,面向过程是从微观角度实现具体细节,两者之间相辅相成

以 每天下楼吃饭为例:

面向过程:                                                              面向对象

1、下楼找餐厅                                                         1、下楼找餐厅

2、看菜品,并熟悉掌握你吃的每一道菜的              2、我要开吃了 (不关注具体菜的细节)

​ 来源,制作流程,烹饪手法等具体细节                  3、吃完了

3、吃这道菜

二十三、Java的面向对象编程

Java作为面向对象的语言,关于面向对象语言的核心概念

1、类和对象

类: 一类事物的抽象的模板,在现实世界中 类就是任意一类事物 ,在程序中类就是一个描述这类事物的类文件。

对象: 在这一类事物中,具体的某一个个体就是对象 ,在程序中对象就是new出来的有内存空间

2、类和对象的关系

类和对象的关系: 类是抽象的而对象是具体的, 对象是由类创建的实例(new出来的)

​ 类的组成(人类):

​ 类名: 给某一类事物取个名字: People

​ 静态的特征称为属性: 姓名,年龄,身高,体重 (定义变量的语法)

​ 动态的行为称为方法: 吃饭,睡觉,打豆豆 (方法的定义依然满足之前所学)

​ 类的实现:

​ 在一个类文件(People)中,定义属性和方法

​ 对象的实现

​ 通过类名创建这个类的对象。

注意 类名不能直接访问 它里面的属性和方法的,必须由类的对象访问

package com.j2008.init;

/**
 * ClassName: People
 * Description:
 * date: 2020/10/7 11:06
 *  创建一个People类  ,并定义这个类的属性(静态的特征)
 *  和 这个类的方法 (动态的行为)
 *
 * @author wuyafeng
 * @version 1.0   softeem.com
 */
public class People {
    // 定义姓名属性   数据类型 和属性名 = [初始值]
    String name="张三";
    // 定义性别属性
    String sex="男";
    // 定义身高属性
    double height =1.75;
    // 定义体重属性
    double weight = 140;

    /**
     * 定义吃饭的行为(方法)
     */
    public void eat(){
        System.out.println("正在吃饭");
    }

    /**
     * 定义睡觉方法
     */
    public void sleep(){
        System.out.println("正在睡觉");
    }

    /**
     * 定义打豆豆方法
     */
    public void playGame(){
        System.out.println("正在打豆豆");
    }
    // 计算两个数相加
    public int add(int a, int b){
        return a+b;
    }


}

public static void main(String[] args) {
         //  不同通过People直接访问它 需要创建类的实例,也就是对象
        //  创建对象的过程称为类的实例化
        // 语法: 类名  对象名   =  new  类名() ;
        People people = new People();
        // 这时候才可以通过对象名 访问这个对象具有的属性 和 方法
         // 对象名.属性名
        // 对象名.方法名([实参])
        System.out.println("这个对象的属性name :"+ people.name);
        System.out.println("这个对象的属性 sex :" + people.sex);
        System.out.println("这个对象的属性 weight :"+ people.weight);
        System.out.println("这个对象的属性height:"+ people.height);
        // 调用对象的访问
        people.eat();
        people.sleep();
        people.playGame();
       int result =  people.add(2,4);
        System.out.println("这个对象还可以计算 结果:"+result);

    }

在类中定义的属性,称为“成员属性” ,在类中定义的方法称为“成员方法”

3、面向对象的特征

​ 面向对象的三大特征: 封装、继承、多态

​ 封装: 将类中成员属性 私有化,并提供公有的访问属性的方法。 为了最大程度保护类中属性的隐蔽性(不被其他对象改变。)

​ 继承: 用于定义类与类的关系的方式 ,一个类可以继承一个类。

​ 多态: 在继承关系中,一个类的对象可能呈现不同的状态。

4、构造器(Construct):

​ 定义: 在创建对象时被自动调用的特殊方法,也称为构造方法。 在一个类中除了包含属性,方法以外,还可以包含 构造器(构造方法)

​ 每一个类都自带一个无参构造器,也可以在这个类中定义多个构造器,多个构造器之间称为“构造器重载”

语法:

  访问修饰符 类名([参数列表]){
      
  } 

例如

public	class	Student{
    
     //无参构造器
    public	Student	(){
        System.out.println("这是一个无参构造器");
    }
}

构造器的作用:

​ 1、用于创建对象自动调用 ,并可以给对象的属性赋初始值

   public	class	Student{
         String	name;//对象的属性
       //有参构造器
       public	Student	(String	name1){
            name=name1;
      }
         //注意一个类中如果存在有参构造器,那么它的无参构造器被覆盖。
  }
创建对象:
   Student	stu	=	new	Student("张三");
     //这里会自动调用有参构造器	并将“张三”赋值给name1,由于自动执行以上构造器,将name1的值赋值给name,这个name就是对象的属性
     System.out.print(stu.name);//张三

类与类的关联关系 :

如果在一个类中引用另一个类,那么这两个类属于关联关系,

例如一个小明同学养了一条狗,如何通过面向对象的方式定义小明同学用于狗的关系

思路: 定义一个People类,其中name属性 ,

​ 定义一个Dog类 包含dogName ,dogColor

​ 将People类与Dog类关联关系 ,在People类中 创建Dog类的引用

public class People {
    String name;
     // 在People类中 建立 People对象与Dog对象的关系
    Dog dog;

    public People(String name){
        this.name = name;  // 将形参name 赋值给 当前对象的成员属性name
    }

}

public class Dog {
    // 由于以下属性 属于狗的特征,所以必须放在Dog类中,而不能放在People类
    String dogName;
    String dogColor;

    /**
     * 创建Dog对象时 初始化狗的基本属性
     * @param dogName
     * @param dogColor
     */
    public Dog(String dogName ,String dogColor){
        this.dogName = dogName;
        this.dogColor = dogColor;
    }
}

 public static void main(String[] args) {
        // 先创建一个小明同学
        People people = new People("小明同学");
        System.out.println("people.dog:"+people.dog);
        // 再创建一个 Dog对象
        Dog dog = new Dog("拉布拉多","灰白");
        System.out.println("dog:"+dog);//设置dog和people对象的关系
        people.dog = dog;
        System.out.println("people.dog:" +dog);

    }
people.dog:null
dog:com.j2008.construct.Dog@4554617c
people.dog:com.j2008.construct.Dog@4554617c

在这里插入图片描述

案例:玩一个小游戏(石头,剪刀,布),定义一个裁判类(Judger),属性包括裁判姓名,两个玩家对象,判断输赢的方法(赢的一方 加1分,输的一方减1分 平的不加不减。), 定义一个玩家类(Player),包含玩家姓名,积分,和出拳的行为(1:石头:2:剪刀:3:步,)并返回出拳值。 通过构造裁判对象时给玩家对象赋值

开始游戏: 创建1个裁判,2个玩家,分布玩10次,最后统计 玩家输赢和得分

二十四、Java的面向对象的特征

1、封装(隐藏)

对类中的成员属性进行隐藏(私有化),对类中的成员方法公共。

2、继承

​ 一个类A可以继承另一个类B,这里类A就是类B的子类,类A可以继承类比的属性和方法,也可以定义自己的属性和方法

3、多态

​ 为了适应需求的多种变化,类可以呈现多种形态,是代码更加通用

1、封装

为了提高类的隐蔽性,对类实现的细节隐藏,提供外部访问的接口即可,提高代码的可扩展性

生活中的封装: 例如笔记本 的内部结构统一封装了,一般人使用笔记本时不需要了解笔记本的结构,而是直接开机关机使用。

对代码的封装包括两层意思:

1、对类的成员属性的封装 :

​ 将属性私有化(private),提供对属性的访问给属性添加公用的getter和setter方法

2、对代码的封装:

​ 为了提高代码的复用性,尽量使用方法加参数传递对代码进行封装,并使该方法公有(public)

public class People {
    private String pname;
    private int age;
    private String sex;
    // 提供 getter 和 setter
    public String getPname(){
        return pname;
    }
    public void setPname(String pname){
        this.pname=pname;
    }

    public int getAge(){
        return age;
    }
    public void setAge(int age){
        // 对成员属性的隐蔽性 可以防止随意对属性更改
        if(age>100 || age<0){
            System.out.println("赋值的年龄不合法");
            return;
        }
        this.age = age;
    }

    public String getSex(){
        return sex;
    }
    public void setSex(String sex){
        this.sex= sex;
    }


    // 通常为了方便给属性赋值,会提供有参构造
    public People(String pname ,int age,String sex){
        this.pname = pname;
        this.age = age;
        this.sex = sex;
    }
    public People(){

    }
 }

​ 对于boolean类型的属性,需要使用isXxx返回属性的值。

封装的优点:

​ 1、良好的封装可以减少类的耦合性(类与类的关联)

​ 2、对类中封装的代码可以自由修改,而不会影响其他类

​ 3、最大程度提高类中属性的隐蔽性 和对属性的控制

2、访问修饰符的权限

​ 用于修饰类,属性,方法的关键字都称为访问修饰符

  1. 1、public :公共的

​ 可被同一个项目的所有类方法(项目可见性)

  1. 2、protected :受保护

​ 可以被自身的类访问

​ 可以被同包下的其他类访问

​ 对于不同包的,存在父子关系的子类可以访问

  1. 3、默认的

​ 可以被自身类访问

​ 可以被同包下的其他类访问

  1. 4、private: 私有的

​ 只能被自身类访问

访问修饰符同一个类同一包不同类(子类或非子类)不同包的子类不同包
public
protected×
默认××
private×××

3、static关键字

​ static表示“静态” ,它可以修饰 属性,方法,代码块 , 在一个类中除了可以定义成员属性、成员方法和构造器以外,还可以定义静态的部分(静态属性,静态方法,静态代码块)

static 修饰属性:称为 静态属性或类的属性

static修饰方法:称为静态方法或类的方法

static修饰的语句块: 称为静态代码块

static修饰的组件不需要通过对象访问,而是直接通过类名访问,在类一加载时会给static修饰的属性和方法分配内存区,这个内存分布在 静态内存区中。后续所有对象操作的是同一个内存区

案例一:

public class Student {
    //  成员属性
    String name;
    // 静态属性  通常static写在public的后面
    static  int age=20;

    // 静态代码块
    static{
        System.out.println("这是静态代码块,在类一加载时,就会被执行,且只执行一次");
    }

    //成员方法  : 既可以访问 成员属性,也可以访问静态属性
    public void getInfo(){
        System.out.println("姓名:"+name +" 年龄:" +age);
    }
    // 静态方法 : 只能访问静态属性,不能访问成员属性(非静态属性
    //   这是为什么? 由于成员属性的存在需要依赖对象 ,
    // 静态属性和静态方法在创建对象之前就必须初始化并分配内存
    public static void getStaticInfo(){
       // System.out.println("姓名:"+name);  成员属性不能被访问
        System.out.println("静态属性:"+age);
    }


    public Student(){
        System.out.println("无参构造器");
    }

    // 构造代码块
    {
        System.out.println("构造代码块");
    }


    public static void main(String[] args) {
        System.out.println("访问静态属性:"+Student.age);
        // 访问静态方法
        Student.getStaticInfo();




    }
       // 类的组件执行顺序
        // 类编译成.class文件被JVM的类加载器加载-》
        // 从上往下初始化static修饰的组件
        // (静态属性,静态代码块,静态方法,其中静态方法调用才执行,静态属性和静态代码块直接分配内存)-》
        //  --》构造代码块-》执行构造器  -》初始化成员属性,成员方法
        Student stu1 = new Student();
        stu1.name="张三";
        // 静态属性可以通过类名访问,也可以通过对象名访问
        stu1.age=21;
        System.out.println(stu1);

        Student stu2 = new Student();
        stu2.name="王麻子";
        stu2.age=22;
        System.out.println(stu2);

        System.out.println(stu1.name);
        System.out.println(stu1.age); //   22

        System.out.println(stu2.name); //王麻子
        System.out.println(stu2.age); // 22

        System.out.println(Student.age);//  22

    }

!传(img-5h6GTsJJ-1602429410497)(assets/11.png)]](https://img-blog.csdnimg.cn/20201011232058612.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMxNjE0MjEx,size_16,color_FFFFFF,t_70#pic_center)

案例二: 静态的变量 在同一个内存中

public class People {
    double height;
    static int score;
    static{
       score++; // 1
    }
   public void setScore(){
       score++; //81  86
   }
   public static void setScore2(){
        score++;
   }

    public static void main(String[] args) {
         People p1 = new People();
         p1.score=80;//静态属性
         p1.setScore();
         People.score=85;
         p1.height= 1.75;
         People p2 = new People();
         p2.setScore(); //86
         p2.height= 1.80;

        System.out.println(p1.score); // 86
        System.out.println(p1.height); // 1.75

        System.out.println(p2.score);//86
        System.out.println(p2.height);// 1.80

    }

}

案例三: 构造代码块和静态代码块 的执行顺序

package com.j2008.statics;

/**
 * ClassName: UserInfo
 * Description:
 * date: 2020/10/8 11:53
 *
 * @author wuyafeng
 * @version 1.0   softeem.com
 */
public class UserInfo {
    //  关于静态的组件 从上往下执行

    // 静态属性  需要先初始化 ,需要new一个对象
    static UserInfo u = new UserInfo(); // 先执行构造代码块 在执行构造器
    static{
        System.out.println("这是静态代码块,只执行一次");
    }

    public UserInfo(){
        System.out.println("这是无参构造器");
    }
    {
        System.out.println("构造代码块");
    }

    public static void main(String[] args) {
         // 结果
        UserInfo u = new UserInfo();
    }


}

结果:

构造代码块
这是无参构造器
这是静态代码块,只执行一次
构造代码块
这是无参构造器

4、继承

4.1、概念

当多个类中都存在相同的属性和行为时,可以将这些共有的属性和行为定义到一个新的类中,让其他类服用这个新类的属性和行为,这种关系就是继承关系

当满足 XXX是 一个XXX的时候,也是继承关系,例如 苹果是一种水果,其中水果就是父类,苹果就是子类, 水果有多种水果,而苹果只是水果的一种,所以苹果继承水果

其中 被继承的类是 父类(超类,基类),继承父类的类就是子类(新类,派生类)

4.2、继承的语法

​ 先定义父类

public  class 父类{
    
}

再定义子类

public class 子类 extends 父类{
    
}

2.1 子类继承父类,子类拥有父类的哪些属性和方法?

可访问: 子类拥有父类的共有的属性和方法, 同包下面的属性和方法,不同包下的受保护的也可以访问

不可访问: 其中子类不能继承 父类的私有的属性和方法、不同包的默认属性和方法 ,不能继承父类的构造器

2.2 子类继承父类,子类如何访问父类的属性和方法

属性: 子类通过super 关键字访问父类的属性 ,子类通过this关键字访问自己的属性

方法:子类通过super挂件自方法父类的方法, 子类通过this关键字访问自己的方法

注意: 这里的this和super可以省略,省略后子类通过“就近原则”访问属性和方法(子类中存在就访问子类的,子类中不存在,就访问父类的。)

super.属性

super.方法(参数)

构造器:子类通过super([参数]) 调用父类的构造器, 子类通过this([参数])调用 自己类的其他构造器,其中 super(参数[]) 必须写在子类构造器的第一行

通过在子类构造器手动调用父类的有参构造器给父类的属性赋值

public class Employee {
    String ename="王麻子";//员工姓名
    double sal=5000 ; // 员工薪资
    
     public Employee(String ename,double sal){
        this.ename = ename;
        this.sal = sal;
    }
 }
public class Manager extends Employee{
    // 奖金
    private double comm;
     public Manager(String ename ,double sal ,double comm){
        // 如何覆盖父类的无参构造器 ,手动调用父类的有参构造
        super(ename ,sal);  // 只能写在第一句
        this.comm = comm;
    }
  }

注意: 子类构造器中默认调用父类的无参构造器

2.3 Java中只能是单继承,一个类只能有一个直接父类的父类,可实现多层次继承

​ 子类 -》 父类 -》 父类的父类

​ pupil -> Student-》 People

创建子类对象时,优先创建父类对象,再创子类对象, 执行顺序 最上层父类的构造器 -》 父类构造器 -》子类构造器。

​ 扩展问题:当一个类中存在static元素时,它们的执行顺序是如何?

顺序: 最上层父类的静态块 - 》 父类的静态块-》 子类的静态块- 》最上层 父类的构造块和构造方法

-》父类的构造块和构造方法- 》 子类的构造块和构造方法

public class People {
    static{
        System.out.println("People的静态语句块");
    }
    public People(){
        System.out.println("People类的无参构造器");
    }
    {
        System.out.println("People的构造语句块");
    }
}

public class Student extends  People{
    static{
        System.out.println("Student 的静态语句块");
    }
    public Student(){
        // 默认调用父类的构造器
        System.out.println("Student的无参构造器");
    }
    {
        System.out.println("Student的构造语句块");
    }
}
public class Pupil extends  Student {
    static{
        System.out.println("Pupil的静态语句块");
    }
    public Pupil(){
        // 调用它父类的无参构造器
        System.out.println("Pupil类的无参构造器");
    }
    {
        System.out.println("pupil的构造器语句块");
    }
}

  public static void main(String[] args) {
          //创建Pupil对象
        Pupil pupil = new Pupil();

    }

结果:

People的静态语句块
Student 的静态语句块
Pupil的静态语句块
People的构造语句块
People类的无参构造器
Student的构造语句块
Student的无参构造器
pupil的构造器语句块
Pupil类的无参构造器

4.3、重写

子类可以继承父类的方法,但是当父类的方法不能满足子类的需要时,子类可以重写父类的方法

重写的必要条件:

1、两个方法名必须相同,且存在不同类中(父子关系的类)

2、子类重写的父类的方法,其方法的参数和返回值必须完全一样,方法的具体实现可不一样

3、访问修饰符必须大于或等于父类的修饰符

注意: 子类的对象 调用父类方法时,如果子类重写了父类的方法,则执行子类的方法,没有重写执行父类的方法。

面试题:

说一下方法的重写与重载的区别?

1、重写存在于父子关系中的不同类,重载存在同类中

2、重写必须满足 方法名相同,且参数相同,返回值相同 ,重载满足方法名相同,参数不同,与返回值无关。

3、重写的访问修饰符必须 子类大于等于父类, 重载没有要求

二十五、抽象类

1、定义

​ 在已有类的基础上,由于特殊情况将该类设置为抽象的,这个类就是抽象类

语法:

public abstract class{
      // 类的元素
}

什么情况下需要定义抽象类?

1、当这个类不需要创建具体的实例时,可将类定义为抽象的

2、当这个类中存在没有实现的方式时(没有方法体的方法),可以将这个类定义抽象的

2、抽象类的特点

​ 2.1 抽象类 不能实例化(不能new) ,通常抽象被当作父类使用

2.2 抽象类中 可以有抽象方法( 没有方法体的方法) 也可以有普通方法

2.3 抽象类被当作父类时,它的子类必须重写父类的抽象方法

public abstract class Fruit {
    private String color;

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    // 水果的甜度  由于不知道是什么水果,所以说过的甜度未知,可以定义为抽象方法
    // 抽象方法
    public abstract void getSweet();

}

public  class Apple extends  Fruit {

    // 子类重写(实现)父类的抽象方法
    @Override
    public void getSweet() {
        System.out.println("这个水果有点甜");
    }
}

public class Lemon extends  Fruit {

    @Override
    public void getSweet() {
        System.out.println("这个水果有点酸,想想都觉得酸");
    }
}
public static void main(String[] args) {
        // 抽象类不能实例化
      //  Fruit fruit = new Fruit();
        // 创建子类对象
        Apple apple = new Apple();
        apple.setColor("红色");
        apple.getSweet();
        System.out.println(apple.getColor());

        Lemon lemon = new Lemon();
        lemon.setColor("黄色");
        lemon.getSweet();
        System.out.println(lemon.getColor());


    }
}

面试题:

抽象方法和非抽象方法的区别

回答: 抽象方法没有方法体,需要使用abstract修饰, 只能写在抽象类中

​ 非抽象方法就是普通方法(成员方法或静态方法) ,可以写在抽象类中或普通类中。

二十六、接口

1、定义

​ 接口用于对某件事物的功能的声明,而没有实现具体功能 ,接口提供对软件开发的标准规范。

​ 利用接口的“多实现”完成Java的单一继承

2、接口语法

public interface 接口名{
      抽象方法定义
}

​ 一个类实现该接口,必须实现接口的所有方法

public class 实现类名  implements 接口名{
     实现接口的所有方法
}

接口的好处:

1、为了规范实现类的行为,让所有的方法都通过接口定义

2、为了提高代码的可扩展性

接口定义实现类的对象 和 实现类定义试下类的对象的区别?

接口定义实现类的对象只能调用接口中定义的方法, 实现类定义的对象既可以调用实现类的方法,也可以调用接口类的方法。

3、接口中的成员组成部分 特点

接口中定义的变量默认全部是 public static final 修饰的

接口中的静态方法可以直接调用。

接口中不能写构造器(因为接口不能实例化,不需要构造器)

接口中的方法全部都是 抽象方法, 在JDK8以后可以定义default的非抽象方法

案例1 : 一个类既可以继承一个类 也可以实现多个接口

//定义一个门类 ,定义一个防盗门,它具有防盗的功能,如何防盗 (防盗功能中可以 拍照, 密码锁)
// 门本 身有 开门,关门 功能

案例2: 一个类可以实现多个接口,中间用逗号分隔 (那么这个类要实现接口的所有方法)

/**
 *  程序员身份
 */
public interface Programer {
    public void takeMoney();
}
/**
 * 厨师
 */
public interface Cooker {
    /**
     * 做好吃
     */
    public void makeFood();
}
public interface Driver {
    // 功能
    public void driverCar();

    public void takeMoney();
}

public class People implements Programer ,Cooker ,Driver {

    private String name;
    public People (String name){
        this.name = name;
    }
    @Override
    public void makeFood() {
        System.out.println("做好吃的");
    }

    @Override
    public void driverCar() {
        System.out.println("开车的功能");
    }

    @Override
    public void takeMoney() {
        System.out.println("赚钱的功能");
    }
}

 public static void main(String[] args) {
         People people = new People("袁巍巍");
         people.takeMoney();
         people.makeFood();
         people.driverCar();
    }

案例3:一个接口可以继承 接口,或多个接口(接口可以多继承)

一个员工接口 一个程序员 (由于程序员是员工,所有程序员可以继承员工接口 )

一个人 是员工也是 程序员 (People 是员工 程序员 )

二十七、多态

多态:继Java面向对象中封装,继承之后的第三个特征

生活中的多态: 同一个行为,例如跑,人是两条腿跑,动物是四条腿或两条腿跑,飞的行为不同是事物飞的方式也不同,飞机飞,小鸟飞,无人机飞都不一样,同一种行为对于不同的事物呈现的不同形态就是多态的表现。

1、 定义

​ 同一种行为,具有多个不同的表现形式

2、实现多态的前提

  • ​ 基于继承关系或基于实现关系的
  • 子类或实现类必须对方法进行重写(没有重写的方法 不具有多态行为)
  • 父类的引用指向子类对象( 接口的引用指向实现类的对象)

3、多态的对象转型

1、子类对象转父类对象时,称为向上转型是默认转换,自动转型

           Cat cat = new Cat();
           // 猫类 可以是一只动物     an4的本质还是 猫对象
          Animal an4 = cat;    // 子类对象转成父类对象 ,是自动转型
           cat.eat();
           an4.eat();

2、父类的引用转成子类对象,称为向下转型,向下转型需要强转 , 为了避免转换错误,需要先判断数据类型是否匹配

   // 创建一个动物类, 将动物类转成子类引用
           Animal an5 = new Cat();
          //  an5.catchMouse(); // 动物类型对象不能访问 它子类特有的方法
          if(an5 instanceof Cat) {
              Cat cat2 = (Cat) an5;
              cat2.eat();
              cat2.catchMouse();
          }
          if(an5 instanceof  Dog) {
              Dog dog2 = (Dog)an5;
          }else{
              System.out.println("不能转成dog");
          }

instanceof : 判断该对象是否属于 这个类型

​ 为什么需要做类型换行呢? 有时候我们需要调用子类特用的方法时必须用子类的引用。所有多态对象下父类应用需要强转。

为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验,格式如下:

变量名 instanceof 数据类型如果变量属于该数据类型,返回true。

如果变量不属于该数据类型,返回false

4、在多态环境型,方法和属性的调用规则

属性: 当子类和父类中存在相同属性时 ,以 对象的左边引用类型为依据,所谓“看左边”

方法: 以当前new出来的对象为依据,如果方法重写了,就调用子类方法,如果方法没有重写 调用父类方法

静态方法: 无论方法是否有重写, 都已对象左边的引用类型为依据。


public class Student extends  People  {
      int age =20;

    @Override
    public void showAge() {
        this.age++;
        System.out.println("年龄:"+this.age+" ----"+super.age);

    }
    // 重写的静态方法
    public static void  method1(){
        System.out.println("method1------------  重写后的 方法");
    }

}
public static void main(String[] args) {
      People p1 = new People();
      System.out.println(p1.age);// 18
       p1.showAge();  // 对象的本质people  调用people

       People p2 = new Student();
      System.out.println( p2.age);// 18
      p2.showAge(); // 本质student ,且重写了 调Student
      p2.sleep();  // 本质student  ,没重写,调用people

        Student p3 = (Student)p2;
     System.out.println( p3.age); // 20
        p3.showAge(); // 本质student ,且重写了 调Student

        People p4 = p3;
     System.out.println( p4.age); // 18   看左边
        p4.showAge(); // 本质student ,且重写了 调Student

}

结论:

18
年龄:18
18
年龄:21 ----18
睡觉方法。。。。。
21
年龄:22 ----18
18
年龄:23 ----18

public static void main(String[] args) {
      People p1 = new People();
      p1.method1();
      p1.method2();

      People p2 = new Student();
      p2.method1();
      p2.method2();

      Student p3 = (Student)p2;
      p3.method1();
      p3.method2();

}

结论:

method1------------
method2---------
method1------------
method2---------
method1------------ 重写后的 方法
method2---------

总结常见面试题:

1、抽象类与接口的区别

​ 共同点: 它们都是抽象的,都不能实例化

区别:

​ 1、抽象类是类 使用abstract修饰,接口使用interface定义

​ 2、抽象类中可以包含抽象方法和普通方法, 接口中是纯粹的抽象类,都是抽象方法,除使用default定义以外

​ 3、子类继承抽象类,必须实现抽象类的方法,或者自身变为抽象类

​ 接口中是实现关系,实现类必须实现接口的所有方法,接口的变量是public static final

2、重载和重写的区别?

3、在java中如何实现多态(多态的前提条件)?

4、final的作用

​ final修饰变量 定义为常量

​ final 修饰方法 不能被重写

​ final 修饰类 则不能被其他子类继承

二十八、设计模式概念

1、定义

​ Java包含23种设计模式,是一套对代码设计经验的总结,被人们反复利用,多人熟知的代码设计方式。

2、目的:

​ 为了提高代码的可读性,可扩展性以及代码的复用性 ,为了解决在写代码过程中遇到的代码设计问题。

3、设计模式的六大原则

​ 3.1 开闭原则: 对扩展开发,对修改关闭(尽可能对代码少修改)

​ 3.2 里式替换原则: 它是面向对象基本原则之一,任何父类(基类)出现的地方,子类都可以出现,也就是子类可以替换父类的任何功能(体现了父类的可扩展性)

​ 3.3 依赖倒转原则 : 尽可能面向接口编程, 依赖接口而不依赖类

​ 3.4 接口隔离原则: 一个类如果能实现多个接口,尽可能实现多个 ,为了降低依赖,降低耦合

​ 3.5 最少知道原则: 一个实体尽可能少的与其他实体产生相互关联关系,将实体的功能独立

​ 3.6 合成复用原则: 尽量使用合成,聚合的方式,而不使用继承

4、设计模式的分类:

1、创建型模式(5个)

工厂方法模式、 抽象工厂模式、单例模式, 建造者模式,原型模式

2、结构型模式(7个)

​ 适配器模式,装饰模式,代理模式,外观模式,桥接模式,享元模式,组合模式

3、行为型模式(11个)

​ 策略模式,模板方法模式,观察者模式,迭代子模式 ,责任链模式,命令模式,状态模式,访问者模式,中介模式,解释器模式,备忘录模式

5、单例模式

单例模式是创建对象的一种特殊方式,程序从始至终都只创建一个对象叫单例(单实例)

分为两类

1、懒汉式单例

public class Person {
    // 为了不让其他类直接访问该成员  懒汉式单例,在使用时创建对象
    // 1 私有静态变量
    private static Person person=null;

    //2、将构造器私有化
    private Person (){

    }
    //3 提供一个静态的方法,并可返回该类的对象
    public static Person getInstance(){
        if(person==null){ // 第一次访问
            person = new Person();
        }
        return person;
    }

    public void sayHello(){
        System.out.println("sayHello方法");
    }
}

2、饿汉式单例

public class Student {
    //1、 饿汉式单例模式,  在类加载时创建一个对象
    private static Student student = new Student();

    // 2 构造器私有化
    private Student(){

    }
    // 3 提供返回类对象的静态方法
    public static Student getInstance(){
        if(student !=null){
            return student;
        }
         return null;
    }
}

6、工厂方法模式

​ 创建对象的过程不再由当前类实例化,而是由工厂类完成 ,在工厂类中只需要告知 对象类型即可。 工厂模式中必须依赖接口

1、简单工厂模式

​ 以生产 “电脑 ” 为例,电脑有办公的功能 , 可以生产一体机 或 笔记本

代码与静态工厂一样

​ 2、静态工厂模式

在这里插入图片描述

/**

 *  电脑

 */
public interface Computer {
    //电脑办公
    public void work();
}

/**
 * ClassName: PersonComputer
==
 * 笔记本
 */
public class PersonComputer implements  Computer{

    @Override
    public void work() {
        System.out.println("这是笔记本电脑,正在办公");
    }
}


/**



 *  一体机


 */
public class WorkComputer implements  Computer{

    @Override
    public void work() {
        System.out.println("这是一体机正在办公");
    }
}

public class ComputerFactory {

    /**
     * 根据不同的类型 生产不同的产品
     * @param type
     * @return
     */
    public Computer produce(String type){
        Computer computer =null;
        if(type.equals("personComputer")){
            computer = new PersonComputer();
        }else if(type.equals("workComputer")){
            computer = new WorkComputer();
        }else{
            System.out.println("不能生产");
        }
        return computer;
    }
    
     /**
     *  静态工厂方法
     * @param type
     * @return
     */
    public static Computer produce(String type){
        // 定义一个接口的引用    通过接口new 一个实现类的对象
        // 提高扩展性
        Computer computer=null;
         if(type.equals("workComputer")){
             computer = new WorkComputer();
         }else if(type.equals("personComputer")){
             computer = new PersonComputer();
         }else{
             System.out.println("不能创建对象");
         }
         return computer;
    }
}


public class Test1 {
 public static void main(String[] args) {
         // 通过工厂类创建对象
        ComputerFactory factory = new ComputerFactory();
        // 要对象 找工厂
        Computer computer1 = factory.produce("workComputer");
        computer1.work();
        // 创建笔记本
        Computer computer2 = factory.produce("personComputer");
        computer2.work();

        Computer computer3 = ComputerFactory2.produce("workComputer");
        computer3.work();
 }
}

​ 3、工厂方法模式

在这里插入图片描述

public interface Car {
    public void  showInfo();
}

public class AudiCar implements Car {
    @Override
    public void showInfo() {
        System.out.println("这是一台奥迪汽车。。");
    }
}
public class BMWCar implements Car {
    @Override
    public void showInfo() {
        System.out.println("这是一台宝马汽车。");
    }
}
/**
生产汽车的工厂接口
**/
public interface CarFactory {
    public Car produce();
}
public class AudiCarFactory implements  CarFactory {
    @Override
    public Car produce() {

        return  new AudiCar();// 这里AudiCar是Car的实现类
    }
}
public class BMWCarFactory implements CarFactory {
    @Override
    public Car produce() {
        return new BMWCar();// 因为BWMCar是Car的实现类
    }
}

 public class Test1 {
        public static void main(String[] args) {
            //先创建 汽车工厂
            CarFactory bmwFactory = new BMWCarFactory();
            // 这个工厂生产的汽车就是 宝马
            Car bmw = bmwFactory.produce();
            bmw.showInfo();
    
            //这个模式对于同一级别的产品,可扩展性高
            //可以扩展不同品牌的汽车,此时不需要修改代码,只需要增加代码即可
            // 创建一个新的品牌汽车  大众汽车
    
            CarFactory dazhongFactory = new DazhongCarFactory();
            Car car = dazhongFactory.produce();
            car.showInfo();
        }
    }

7、抽象工厂模式

​ 对于在工厂方法的基础上,对同一个品牌的产品有不同的分类,并对分类产品创建的过程 ,一个汽车产品 会分为不同的种类(迷你汽车 , SUV汽车 )

/**
 * 迷你汽车接口
 */
public interface MiniCar {
    public void showInfo();
}

public interface SUVCar {
    public void showInfo();

}
public class AudiMiniCar implements  MiniCar {
    @Override
    public void showInfo() {
        System.out.println("这是奥迪迷你汽车 ");
    }
}
public class BMWMiniCar implements  MiniCar {
    @Override
    public void showInfo() {
        System.out.println("这是宝马Cooper迷你汽车");
    }
}
public class AudiSUVCar implements  SUVCar {
    @Override
    public void showInfo() {
        System.out.println("这是一辆 奥迪SUV汽车");
    }
}
public class BMWSUVCar implements  SUVCar {
    @Override
    public void showInfo() {
        System.out.println("这宝马的SUV系列");
    }
}

public interface CarFactory {
    //生成不同型号的汽车 ,两条产品线
    public MiniCar produceMiniCar();

    public SUVCar produceSUVCar();
}
public class AudiCarFactory implements  CarFactory {
    @Override
    public MiniCar produceMiniCar() {
        return new AudiMiniCar();
    }

    @Override
    public SUVCar produceSUVCar() {
        return new AudiSUVCar();
    }
}
public class BMWCarFactory implements  CarFactory {
    // 生成迷你汽车的方法,返回MiniCar
    @Override
    public MiniCar produceMiniCar() {
        return new BMWMiniCar();
    }
    //生成SUV汽车的方法, 返回SUVCar
    @Override
    public SUVCar produceSUVCar() {
        return new BMWSUVCar();
    }
}

public class Test1 {
    public static void main(String[] args) {
        //创建宝马迷你汽车  找工厂
        CarFactory factory = new BMWCarFactory();
        MiniCar car = factory.produceMiniCar();
        car.showInfo();
    }
}

总结: 对于简单工厂, 工厂方法模式和抽象工厂的区别和用途

1、对于简单工厂(静态和非静态),用于生产同一结构中的任意产品,对于新增产品不适用。

2、对于工厂方法,在简单工厂的基础上,生产同一个等级结构中的固定产品,可以支持新增产品。

3、抽象工厂: 用于生产不同种类(品牌)的相同类型(迷你,SUV) ,对于新增品牌可以,不支持新增类型。

8、模板方法

​ 定义:

​ 模板方法是一种行为模式,父类的一个方法定义完成这个方法的步骤,但不具体实现具体细节,由子类完成各个步骤的实现,在创建子类对象时,最终的实现过程是子类的方法。

​ 模板方法的准备:

​ 1、继承关系

​ 2、父类是抽象类 :抽象类实现了模板方法,定义了算法的估计

3、子类继承抽象类:实现抽象方法,完成完整的算法

public abstract class AbstractPerson {
    /**
     *  定义一个模板方法,用于实现这个方法的基本“骨架”
     *  每一步骤的具体实现由子类完成
     */
    public void preparedSchool(){
        getUp();

        dressing();

        eat();

    }
    //起床
    public abstract void getUp();
    //穿衣服
    public abstract void dressing();
    //吃早餐
    public abstract void eat();

}

public class Teacher extends  AbstractPerson {
    @Override
    public void getUp() {
        System.out.println("老师起床,7点半起床");
    }

    @Override
    public void dressing() {
        System.out.println("老师要衣服得体,穿工装");
    }

    @Override
    public void eat() {
        System.out.println("老师吃早餐。");
    }
}
public class Test1 {
    public static void main(String[] args) {
          Student stu = new Student();
          stu.preparedSchool();

          Teacher teacher = new Teacher();
          teacher.preparedSchool();

    }
}

二十九、内部类(inner class)

1、定义

​ 在一个类中,定义另一个类的代码结构,通常定义在类内部的类称为 “内部类” ,外面的类称为“外部类” , 在逻辑关系上 内部类与外部类是从属关系,比如 一个People类 存在收货地址类(收货人,收货联系方式)

2、分类

​ 2.1、 普通内部类(inner class),一个类A中定义另一个类B,其中类B就是类A的内部类,也是类A的一部分

public class People {
    private String pname="张三";

    public void sayHello(){
        System.out.println("Let us say Hello");
        // 知识点1 :外部类的方法中,可以使用内部类的属性、方法
        Address address = new Address();
        address.addressName ="湖北武汉";
        address.contentName="张某某";
        address.showAddressInfo();
    }


    /**
     * 定义普通内部类   收货地址
     */
    class Address{
        private String addressName;// 收货地址
        private String contentName;// 联系人

        public void showAddressInfo(){
            System.out.println("联系人:"+contentName + "--收货地址:"+addressName);
            // 内部类的方法 可以直接访问外部类的属性  (由于通常情况属性的访问必须通过对象才可以使用,而)
            System.out.println("访问外部类的属性:"+pname);
        }


    }



}

​ 注意两点

  • ​ 外部类的方法中,可以直接访问内部类的所有成员(包括私有)
  • ​ 内部类的方法中,也可以直接方法外部类的所有成员,当外部和内部的成员名相同时,就近原则访问成员,或者引入外部类的对象访问

​ 2.2、 静态内部类(static inner class): 在普通内部类基础上,增加“static”关键字,与静态方法相似,满足静态的要求

public class People{
/**
     * 2、定义静态内部类
     *   卡信息
     */
    static  class Card{
        private static String cardNo="4200018888000022";
        private String cardName="身份证";

        // 定义静态方法
        public static void showCard(){
            System.out.println("身份证号:"+ cardNo);
        }
        // 定义非静态方法
        public void showCard2(){
            System.out.println("cardName:"+cardName + "----"+ cardNo);
        }
   
    }

	// 外部类的方法 
    public void method2(){
        Card card = new Card();
        // 对于静态方法可以直接类名.方法名
        // 对于非静态方法,需要创建Card类的对象访问
        card.showCard2();


    }
}

使用:
        // 2 创建静态内部类的对象
        People.Card.showCard();
        // 创建静态内部类的对象
        People.Card  card = new People.Card();
        card.showCard2();

​ 2.3、方法内部类: 在一个方法中定义的类,其中这个类只属于该方法,也只能在该方法中使用

 /**
     * 3、方法内部类 (将一个类定义在方法里面)
     */
    public void method3(){
         int score = 98;

         // 在这里定义一个类
        class MyClass{
            String subject="Java";
            public void getSubjectScore(){
                //方法内部类中 也可以使用方法的属性
                System.out.println(pname+"的"+subject+":"+score);
            }
        }

        //调用方法里面的类
        MyClass  mycls = new MyClass();
        mycls.getSubjectScore();
    }
    People  people = new People();
    // 3 调用方法
        people.method3();

注意:内部类剩的class文件 命名 外部类$内部类名.class

2.4 匿名内部类: 定义一个没有类名,只有对方法的具体实现。通常它依赖于实现关系(接口)或继承关系(父类)

a、基于实现关系

public interface MyInterface {
    // 学习
    public void  study();
    //  工作
    public void work();
}

  // 创建一个匿名类(让接口的引用 指向匿名类的对象)
        MyInterface  person = new MyInterface() {
            @Override
            public void study() {
                System.out.println("这个人也好好学习");
            }

            @Override
            public void work() {
                System.out.println("这个人也好好工作");
            }
        };

        person.study();
        person.work();

b、基于继承关系

public class MyClass {

   public void service(){
        System.out.println("提供服务的方法。");
    }
}
 // 父类 new 一个 匿名类,这个匿名类是它的子类
        MyClass cls = new MyClass(){
            @Override //匿名类重写父类的方法 service
            public void service() {
                System.out.println("这是子类的方法");
            }
        };
        cls.service();

三十、异常

1、异常的概述

​ 异常定义: 在程序中,发生“不正常”的事件,导致程序无法正常运行,并使JVM中断,称为异常

​ 生活中的异常: 早上起床上课,平时骑车20分钟可以到达教室,由于天气原因或者闹钟响了自动关闭,不能按时到达教室上课,迟到了,此时就属于异常现象 。

​ 捕获异常: 当程序在运行时,发生了异常 ,为了让程序正常执行,需要对异常捕获(catch),称之为捕获异常

​ Java是面向对象的语言, 异常本身就是一个类(Exception),当发生异常时会创建异常对象,捕获的就是该对象。

  System.out.println("请输入一个数字");
        Scanner sc = new Scanner(System.in);
        // 对可能发生的异常 进行处理
        int num = sc.nextInt();
        if(num%2==0){
            System.out.println("这个数是偶数");
        }

​ 异常代码可能发生异常, 当用户输入非数字时, 导致程序抛出一个异常对象 :

Exception in thread "main" java.util.InputMismatchException
	at java.util.Scanner.throwFor(Scanner.java:864)

2、异常关键字 以及层次关系

​ a、try: 试一试 ,将可能发生的代码使用try包裹 ,try不能单独出现

​ b、catch : 捕获异常, 当发生指定的异常对象时,执行catch代码

  System.out.println("请输入一个数字");
        Scanner sc = new Scanner(System.in);
        // 对可能发生的异常 进行处理
        try {
            int num = sc.nextInt();  // 发生异常后,try里面的代码不再执行
            if (num % 2 == 0) {
                System.out.println("这个数是偶数");
            }
            System.out.println("结束");
        }catch(Exception ee){// 对应的异常类 来捕获对应的异常对象  ,不能确定异常类,可以使用父类Exception
            System.out.println("你的输入不正确");
        }

        System.out.println("程序继续运行直到结束。。。。");

​ 一个try + 多个catch

 	//  抛出的异常 不能被catch捕获,会发生什么?
        try {
            int[] num = {1, 2, 3};
            System.out.println(num[1]); // 没有捕获该异常对象,JVM依然终止运行
            System.out.println(10/0);
        }catch(NullPointerException ee){
            System.out.println("这是空指针异常");
        }catch(ArrayIndexOutOfBoundsException  ee){
            System.out.println("数组下标越界异常");
        }catch(Exception ee){
            // 输出异常 堆栈消息  方便程序员排错(尽可能避免用户看见)
            ee.printStackTrace();
            System.out.println("系统繁忙!"+ee.getMessage());
        }
        System.out.println("程序结束");

​ c: finally : 异常之后的最终处理 (无法是否发生异常,程序都执行 )

​ try… finally 结构

	 try{
            System.out.println("请输入两个数 ,计算两个数相除");
            Scanner sc = new Scanner(System.in);
            int  num1 =  sc.nextInt();
            int num2 = sc.nextInt();
            double  s = num1/num2; // 可能出错
            System.out.println(" try里面结束,结果:"+s);
        }finally{
            System.out.println("无论是否发生异常,都会执行这个语句块,一般用于资源回收");
        }

try… catch…finally 结构

 try {
            System.out.println("请输入两个数 ,计算两个数相除");
            Scanner sc = new Scanner(System.in);
            int num1 = sc.nextInt();
            int num2 = sc.nextInt();
            double s = num1 / num2; // 可能出错
            System.out.println(" try里面结束,结果:" + s);
        }catch(ArithmeticException ee){
            ee.printStackTrace();
            System.out.println("除数不能为0 !!");
        }catch(Exception ee){
            ee.printStackTrace();
            System.out.println("系统繁忙!!!");
        }finally {
            System.out.println("用于资源回收。");
        }

3、捕获异常

​ try…catch…finally

4、抛出异常

  /**
     * 根据下标访问数组元素
     * @param array
     * @param index
     * @return
     */
    public static int getEleByIndex(int [] array , int index){
         // 抛出异常: 可以在异常发生时 或发生之前 创建一个异常对象并抛出
        //  手动抛出一个异常  throw new 异常类([异常消息]);
        if(index <0 || index > array.length-1){
            //抛出异常
            throw new ArrayIndexOutOfBoundsException("你的下标越界了");
        }
        int n =  array[index];
        return n;
    }

public static void main(String[] args) {
          //数组
        int  [] array = {2,1,4,5};
        int index=4;
        // 定义方法访问下标的元素  此时会产生异常 并抛出给方法的调用者
        try {
            int num = getEleByIndex(array, index);
            System.out.println("访问的元素:" + num);
        }catch(ArrayIndexOutOfBoundsException ee){
            System.out.println(ee.getMessage());
        }
        System.out.println("结束。。。");

    }

5、异常分类

由于有些异常是不能直接抛出的 ,需要先声明才可以抛出,异常可以分为两大类:

1、 编译期异常(check 异常或者检查异常):在编译期间检查异常,如果没有处理异常,则编译出错。

        //创建一个文件类的对象
        File  file = new File("d:/aaa.txt");
         // 在写代码(编译之前)时 一定要处理的异常(try..catch 或者 throws),就是编译时异常 
        try {
            file.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }

​ 这里的IOException 就是 编译期异常,需要手动处理的

2、运行期异常(runtime 异常或者运行异常):在运行期间检查异常, 编译期可以不处理异常。

        // 在运行期间抛出异常  不需要事先处理的  NullPointException是运行异常
        String str=null;
        System.out.println(str.length());

(img-RKXVJlJK-1603596724561)(assets/1.png)]

Exception中常用的异常类

  • ​ RuntimeException
    • ArrayIndexOutOfBoundsException :数组下标越界异常
    • NullPointerException:空指针异常
    • ArithmeticException: 算术异常
    • NumberFormatException :数字格式化异常
    • ClassNotFoundException: 类没找到异常
    • ClassCaseException: 类转换异常
  • 检查异常(check Exception)

​ IOException :IO操作

​ FileNotFoundException: 文件未找到异常

​ SQLException:

​ EOFException:读写文件尾异常

​ DateFormatException:日期格式化异常

​ SocketException:SocketException

注意: 对于抛出检查异常,需要使用throws声明,对于抛出运行时异常,必须要使用throws声明

声明抛出异常语法:

 声明抛出异常语法:  
         public ...  方法名([参数]) throws 异常类1,异常类2{
             
              // 通过throw抛出   或 处理 检查异常 
      }
  /**
     *   声明抛出异常语法:
     *       public ...  方法名([参数]) throws 异常类1,异常类2{
     *
     *       }
     */
    //创建文件
    public  static void createFile() throws FileNotFoundException ,IOException {
        File file = new File("d:/hello.txt");
        if(file.exists()){
             // 不能创建  ,需要提示用户  该文件存在
            throw new FileNotFoundException("这个文件已存在,不能创建");
        }else{
            //创建
            file.createNewFile();
        }
    }

面试题: 关于 finally 和 return的执行顺序问题?

回答: 当方法有返回值时,先执行fianlly,再return, 但是 finally的代码不会改变return结果


    /**
     *  方法有返回值 有 finally
     * @param n
     * @return
     */
    public static  int  getNum(int n){
          try{
                if(n%2==0){
                    n++;
                }else{
                    n--;
                }
      
             return n;    
          }catch(Exception ee){
              System.out.println("catch--"+n);
            return 0;
          }finally {
              // return 如果放在 try或catch中,不会受finally的改变
              //  如果放在最下面,会受finally的改变
              n++; // 5
              System.out.println("fially----n:" + n); // 5
          }
    }

结果 返回

fially----n:5
4

6、自定义异常

1、为什么需要使用自定义异常

​ 在Java中每一个异常类都表示特定的异常类型, 例如 NullPointerException表示空指针 ,ArithmeticException表示算术异常, 但是sun公司提供的API中不可能将实际项目中的业务问题全部定义为已知的异常类 ,这是需要程序员根据业务需求来定制异常类,例如 用户注册,可以定义用户注册异常(RegisterException),分数不能为负数也可以定制异常(ScoreExcecption)。

2、什么是自定义异常

​ 在开发中根据自己的业务情况来定义异常类 , 灵活性较高,且方便易用。

3、如何实现自定义异常

​ a、定义编译期异常类,创建一个类继承 java.lang.Exception ;

​ b、定义运行期异常类,创建一个类继承java.lang.RuntimeException;

4、案例分析:自定义异常应用

​ 要求: 模拟用户注册操作, 用户输入用户名 ,验证用户名是否存在,如果存在,则抛出一个异常消息 “亲,该用户已存在,不能注册” ,通过自定义异常提示消息

public class RegisterException  extends  Exception {
    public RegisterException(){

    }

    public RegisterException(String message){
        // 将message 赋值给父类的构造
        super(message); //  将message赋值给父类的 属性,可通过getMessage()方法

    }
}

public class TestRegister {
     // 模拟已存在的用户
    String []  users = {"袁魏巍","王麻子","王小花"};

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入你要注册的用户:");
        String uname = sc.next();
        TestRegister obj = new TestRegister();

        try {
            // 调用方法
            obj.checkUserName(uname);
            System.out.println("注册成功");
        } catch (RegisterException e) {
            System.out.println("注册失败");
            System.out.println(e.getMessage());
        }


    }

    /**
     * 检查用户是否存在
     * @param username
     * @return   true  表示通过
     *    异常表示不通过
     */
    public boolean  checkUserName(String username) throws RegisterException{
         // 使用foreach遍历
        /**
         *   for(数据类型 变量名  : 数组名/集合名 ){
         *        循环中的 变量名代表的就是数组的元素
         *   }
         */
        for(String  u : users){
            // 判断u是否与 username相等 ,相等说明用户存在,需要抛出异常
            if(u.equals(username)){
                throw new RegisterException("亲,"+username+" 已存在,不能注册");
            }
        }
        return true;

    }

}

三十一、包的结构与功能介绍

Java是一门面向对象的语言,sun公司提供基于面向对象的帮助文档(API Application Program Interface) ,并针对不同的版本生成的API

API中根据不同的功能分如下包 (package)

  • java.applet.* : java的小应用程序
  • java.awt.* 和 java.swing.* : java的图形用户界面(开发单机版的小游戏)
  • java.lang.* : java的语言包
  • java.util.* : java的工具类包、集合框架包
  • java.io.* : java文件读写包(Input 、Output)
  • java.net.* : java的网络编程包(Socket机制相关,URL)
  • java.sql./ javax.sql. : java的数据库操作
  • java.lang.reflect.* 反射相关包

三十二、java的lang包

1、包装类

​ 定义: Java的8个基本数据类型对应的 对象类型,称为它们的包装类

​ 为什么需要包装类:

​ 基本数据类型中不能提供方法, 不能与其他引用数据类型转换,此时包装类作为该基本数据类型的对象类型,不仅提供常用的方法,还可以与其他数据类型互相转换 和 “装箱”、“拆箱”

基本数据类型包装类型包装类的默认值
byteBytenull
shortShortnull
intIntegernull
longLongnull
floatFloatnull
doubleDoublenull
charCharacter
booleanBoolean

​ 问题1: 基本数据类型、包装类以及字符串的相互转换

在这里插入图片描述

public static void main(String[] args) {
        // 1、byte 的包装类   Byte
        // 创建包装类的对象
        byte b=123;
        Byte obj1 = new Byte(b);
        //1、 包装类 转字符串   包装类对象.toString()
        String s1 = obj1.toString();

        //2、字符串转包装类      new 包装类(s) 或者   包装类.valueOf(s)
        String s2="100";
        Byte obj2 = new Byte(s2);
        // 或者
        Byte obj3 = Byte.valueOf(s2);



        //3  获取包装类的数值,包装类转基本数据类型  Byte  - >  byte
        //  包装类.valueOf(基本数据类型) 或者 byteValue()
        byte b2 = obj2;  // 包装类可以直接复制给基本数据类型  ,这个过程 “拆箱”过程
        byte b3 = Byte.valueOf(obj2);

        // 4、字符串转 基本类型     包装类.paseByte(s)
        byte b4 = Byte.parseByte(s2);


        byte b5=122;
        String s5 = new Byte(b5).toString();


    }

再以 Integer 举例

public static void main(String[] args) {

        int n=250;
        // 转包装类
        Integer obj1 = new Integer(n);
        //包装类转基本数据类型
        int n3 = Integer.valueOf(obj1);

        // 转字符串
        String s1 = obj1.toString();
        //  字符串再转成  Integer
        Integer obj2 = Integer.parseInt(s1);
        Integer obj3 = new Integer(s1);

        // 字符串转int
        int n2 = Integer.valueOf(s1);
        // int  转 转字符串
        String s3 = new Integer(n2).toString();

        System.out.println("-------------Intger的常用方法------");
        int num = Integer.bitCount(2);  // 个位数 + 高位数的和
        System.out.println(num);
        // n1>n2 返回1   n1==n2 返回0   n1<n2 -1
        //比较两个数是否相等
        System.out.println(Integer.compare(100,200));

        System.out.println(Integer.decode("123"));

        //equals  比较两个数是否相等  对于基本数据类型的包装类比较他们的数值
        Integer  n1  = new Integer(90);
        Integer n4 = new  Integer(90);
        System.out.println(n1.equals(n4));// 比较两个对象的 值
        System.out.println(n1 == n4);// 比较 两个对象的地址

        int n5 =100;
        int n6 =100;
        System.out.println(n5==n6);// 对于基本数据类型 == 比较的值


        // 进制转换
        System.out.println(Integer.toBinaryString(18));//转成二进制表示形式
        System.out.println(Integer.toHexString(15));//转成16进制表示形式
        System.out.println(Integer.toOctalString(10));//转成8进制表示

    }

​ 问题2: 数据类型的装箱和拆箱

​ 装箱: 将基本数据类型自动转换成 它的包装类,可以使用包装类的方法和属性

 // 自动装箱: 100自动转成 Integer 
        Integer num1 = 100;

​ 拆箱: 将包装类型 自动转成 对应的基本数据类型。

// 自动拆箱:  Integer 自动转成  int
        int num2 = num1;

面试题:

  public static void main(String[] args) {
        // 包装类
        // 自动装箱: 100自动转成 Integer
        Integer num1 = 100;
        // 自动拆箱:  Integer 自动转成  int
        int num2 = num1;

        Integer n1 =100;
        Integer n2 =100;
        System.out.println(n1.equals(n2)); //true
        System.out.println(n1 == n2);// 应该true   他们同时指向常量池100


        Integer n3 = 150; // 等价于 Integer n3 = new Integer(150);
        Integer n4 = 150; // 等价于 Integer n4 = new Integer(150);
        System.out.println(n3.equals(n4));//true
        System.out.println(n3 == n4);//false

        Integer n6 = new Integer(100);// 一定会创建新对象
        System.out.println(n6 == n1); // false

    }
        //结论
		//对于    -128 <=Integer的值 <=127 之间(byte范围),
        // 装箱时不会创建新对象 而是直接引用 常量池中的值
        // 如果超出byte 的返回,则自动创建新对象,各自指向新对象的内存

2、Object类

​ Object类是lang包提供的 ,对于lang包的类不需要import,所以 Object类无处不在,你不需要自己创建

​ 常用方法

​ a、getClass: 返回该对象的类型 任何类都有它的类型

​ b、equals : Java中所有的equals 方式都是重写Object的方法

​ 原生的equals 比较的是 对象的地址 ,我们通常说的 equals比较两个对象的值是因为几乎所有的数据类型(包装类,String)都重写了equals 方法的

 public boolean equals(Object obj) {
        return (this == obj);
    }

​ c、 hashCode() : 返回该都对象的hash值

  // Object中的hashCode 本身没有实现 ,
        /**
         * 1、对于基本数据类型的包装类 其值就是其本身
         * 2、对于String类型的HashCode ,也是String自己实现的,其算法目的尽可能减少hash冲突
         * 3、对于自定义类,需要你自己重写HashCode ,如果不重写 就在程序运行期间 JVM根据内存地址
         *    类自动分配。(原则: 根据每个有意义的属性值,计算各自的hashCode 相加等一系列操作得到)
         */

​ d:finalize() 资源回收调用该方法, 当对象地址不在被引用时,会被GC回收 并调用该方法

​ Object obj = null ;

​ c:toString() : 返回该对象的字符串表现形式 (通常会被子类重写)

​ wait():线程等待

​ notify():唤醒其中一个等待的线程

​ notifyAll:唤醒所有等待中的线程

对象的比较

public class Student {
    private  int id; //学生编号
    private String sname;
    private Integer age;

    public void showInfo(){
        System.out.println( sname +"---"+ age);

    }

    public Student(){

    }
    public Student(int id ,String sname ,int age){
        this.id = id;
        this.sname = sname;
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
        if(this == obj){
            return true;
        }
        // 判断类型 是否一致
        if(obj  instanceof  Student){
            // 强转
            Student stu = (Student)obj;
            // 开始比较 id 和 sname
            if(this.id == stu.id &&  this.sname.equals(stu.sname)){
                    return true;
            }
        }
        return false;


    }

    @Override
    public int hashCode() {
        return id;
    }
}

 public static void main(String[] args) {
          // 创建对象   比较对象是否相等
        // 比较内存相等 或 比较值(对象的属性)相等
        Student stu1 = new Student(1001,"敖园",22);
        Student stu2 = new Student(1001,"敖园",22);
        System.out.println(stu1==stu2);  // 比较两个对象的地址 (不相等)    false
        System.out.println(stu1.equals(stu2));   // true
        // 由于equals本身没办法解决
        //    两个对象因id 和name相等业务上是同一个对象的问题
        // 所以需要重写 equals 和 hashcode 。
         // 为什么要重写HashCode呢?
        //  回答: 在JMV中如果HashCode不相等,一定不能认为是同一个对象

        Student stu3 = stu1;  // stu3 的地址于stu1的地址是同一个

    }

总结: 对象之间的比较 ,通常是比较属性值,如果属性值相等,我们可以认为是同一个对象,

此时需要重写 equals 和hashcode方法。

为什么要重写HashCode呢?

回答: 在JMV中如果HashCode不相等,一定不能认为是同一个对象

3、System类

 public static void main(String[] args) {
        // System 属于系统类
       //  System.out; // 获取控制台的打印流
        // 设置JVM运行时 系统参数
        System.setProperty("encoding","UTF-8");
        System.out.println("获取:"+System.getProperty("encoding"));
        // 时间从 1970-01-01
        System.out.println("获取当前系统的时间毫秒数:"+ System.currentTimeMillis());

        System.exit(0); // 0 : 表示JVM正常退出    -1 表示非正常退出

    }

4、字符串类

​ java.lang.String类,Java中所有的字符串都会创建该类的实例 , 它可以对字符串查找,检索,转变大小写,截取等一系列操作,并提供了大量的字符串操作方法。

String类的特点:

​ 它是一个不可变字符串 ,它的值创建后不能被改变。

String的构造器

      // 创建字符串对象
        String s1="abc";
        String s2 = new String("abc");
        //通过字符数组构建
        char [] chars = {'a','b','c'};
        String s3 = new String(chars);  //  或指定长度构建字符串
        String s4 = new String(chars,0,2);
        //或根据字节数组构建
        byte [] byts = {97,98,99};
        String s5 = new String(byts);

        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s3);
        System.out.println(s4);
        System.out.println(s5);

  // 字符串是一个不可变的对象
        // class类被JVM装载时,会分配一个存放字符串的常量池(String Pool)
        // 在类加载时 先检查常量池中是否有“abc”常量,如果有,直接将ss1指向该常量
        // 如果没有,则创建常量abc
        // 创建2个对象
        String ss1 = "abc";
        //  abc常量不能改变,  则再创建 abcd的常量,由ss1重新指向
        ss1+="d";
 // 创建3个对象
        String ss2 ="abcd";  // abcd
        String ss3 = "aaa";  // aaa
        ss2 += ss3;  // abcdaaa   重新创建abcdaaa并由ss2重新指向

    String a1="abc";
        String b1="abc"; // 两个地址同时指向一个常量 “abc”
        System.out.println(a1==b1);  // true
        System.out.println(a1.equals(b1));

        String c1=new String("abc");// 堆内存中  对abc包装后的地址
        System.out.println(a1==c1);  // false
        System.out.println(a1.equals(c1));//true

在这里插入图片描述

字符串类常用方法

  • 将此字符串与指定对象进行比较:public boolean equals (Object anObject)
  • 将此字符串与指定对象进行比较,忽略大小写:public boolean equalsIgnoreCase (String anotherString)

举例:

	public static void main(String[] args) {
		String s1 = "hello";
		String s2 = "hello";
		String s3 = "HELLO";
		
		//boolean equese(Object obj):比较字符串的内容是否相同
		System.out.println(s1.equals(s2));
		System.out.println(s1.equals(s3));
		System.out.println("-------------");
		
		//boolean equalsIgnoreCose(String str):比较字符串的内容是否相同,忽略大小写
		System.out.println(s1.equalsIgnoreCase(s2));
		System.out.println(s1.equalsIgnoreCase(s3));
		System.out.println("--------------");
	}
4.1、获取功能的方法
  • 返回字符串的长度:public int length()
  • 将指定的字符串连接到该字符串的末尾:public String concat (String str)
  • 返回指定索引处的char值:public char charAt (int index)
  • 返回指定字符串第一次出现在该字符串内的索引:public int indexOf(String str)
  • 返回一个子字符串,从beginIndex开始截取字符串到字符串结尾:public String substring (int beginIndex)
  • 返回一个子字符串,从beginIndex到endIndex截取字符串。含beginIndex,不含endIndexpublic String substring (int beginIndex,int endIndex)

举例:

	public static void main(String[] args) {
		
		String s = "helloworld";
		//length() :获取字符串的长度,其实也就是字符的个数
		System.out.println(s.length());
		System.out.println("---------");
		
		//String concat (String str):将指定的字符串连接到该字符串的末尾
        String s2 = s.concat("**hellow itheima");
        System.out.println(s2);
        
        //charAt(int index):获取指定索引处的字符串
        System.out.println(s.charAt(0));
        System.out.println(s.charAt(1));
        System.out.println("-------");
        
        //int indexOf(String str):获取str在字符串对象中第一次出现的索引,没有返回-1
        System.out.println(s.indexOf("l"));
        System.out.println(s.indexOf("owo"));
        System.out.println(s.indexOf("ak"));
        System.out.println("---------");
        
        //String sbustring(int start):截取从start开始,到字符串结尾的字符串
        System.out.println(s.substring(0));
        System.out.println(s.substring(5));
        System.out.println("----------");
        
        //String substring(int start,int end):从start到end截取字符串,含start,不含end
        System.out.println(s.substring(0,s.length()));
        System.out.println(s.substring(3,8));

	}
4.2、转换功能的方法
  • 将字符串转换为新的字符数组:public char[] toCharArray()
  • 使用平台的默认字符集将该String编码转换为新的字节数组:public byte[] getBytes()
  • 将与targer匹配的字符串使用replacement字符串替换public String rep;ace (CharSequence targer,CharSequence replacement)

举例:

	public static void main(String[] args) {
		
		String s = "helloworld";
        
        //char[] toCharArray():把字符串转换为字符数组
        char[] chs = s.toCharArray();
        for(int x = 0 ; x < chs.length;x++){
        	System.out.println(chs[x]);
        }
        System.out.println("---------");
        
        //byte[] getBytes():把字符串转换为字节数组
        byte[] bytes = s.getBytes();
        for(int x = 0;x < bytes.length; x++){
        	System.out.println(bytes[x]);
        }
        System.out.println("--------");
        
        String str = "softeem";
        String replace = str.replace("s","S");
        System.out.println(replace);
	}
4.3、分割功能的方法

将字符串按照给定的regex(规则)拆分为字符串数组:public String[] split(String regex)

举例:

public static void main(String[] args) {
		String s = "aa|bb|cc";
		String[] strArray = s.split("|");
		for(int x = 0;x < strArray.length; x++){
			System.out.println(strArray[x]);
		}
	}

三十三、StringBuffer和StringBuilder类

1、StringBuffer 类

​ 是一个字符串缓冲区的类,线程安全运行效率低,用户存储可变字符串

​ 构造器:

  StringBuffer  sb = new StringBuffer(); // 创建空字符串的容器
  StringBuffer sb = new StringBuffer(String);// 将字符串使用容器存储
  StringBuffer sb = new StringBuufer(int);//声明指定容量的容器

​ 常用方法:

1.1、append():追加字符串

1.2、delete(int start,int end):删除指定位置的字符

1.3、insert(int start ,String):插入到指定位置

1.4、reverse():反转字符

1.5、capacity():获取初始容量

1.6、ensureCapacity(int):设置最低所需容量

2、StringBuilder类

也是字符串缓冲区的类,它是线程不安全,且运行效率高的可变字符串缓冲类

在这里插入图片描述

其StringBuilder的方法与StringBuffer几乎一样

3、面试题

1、StringBuffer、StringBuilder和String的区别

​ a、在运行速度上 : StringBuilder > StringBuffer > String

​ 原因: String是字符串常量,而StringBuilder和StringBuffer是字符串变量,当需要改变字符串内容时,Stirng重新创建变量并赋值, 而StringBuilder和StringBuffer可直接改原有的值,所有效率高,

​ b、在线程安全上: StringBuffer > StringBuilder > String

​ 原因: StringBuffer是线程安全的,而StringBuilder线程不安全,在StringBuffer上的很多方法增加同步关键字(synchronized),导致在多个线程运行时,保持数据的完整性和一致性,而StringBuilder的方法并没有同步 ,如果在多线程环境下为了确保数据安全,建议使用StringBuffer ,如果在单线程环境下,提高效率使用StringBuilder。

三十四、对象的克隆

1、为什么需要克隆?

​ 对于基本数据类型,可以将值直接复制给另一个变量,这里两个变量相互独立,而引用数据类型(自定义类) 对于引用类型的赋值并没有产生新的个体,而是将两个变量的类型指向同一个对象。 (本质只有一个对象),如果想要赋值的对象与原始对象独立,则需要进行“对象克隆”

2、如何克隆

​ 我们知道任意一个类都继承自Object类,其中Object类提供一个clone方法 用于克隆对象。

​ 实现步骤:

​ a、 实现 接口 Cloneable

​ b、重写 clone方法(由于该方法是Object的 protectect修饰 不能直接访问)

3、浅克隆和深克隆

3.1、浅克隆

在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。

Java的对象克隆默认是浅克隆,

在这里插入图片描述

3.2、深克隆

在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。

简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。

在这里插入图片描述

实现深克隆的方式:

​ 1、需要将克隆对象的引用数据类型 也实现克隆

public class Student implements  Cloneable {
    private int id;
    private String sname;
    private int age;
    //收货地址
    private Address address;
    //实现深克隆
      @Override
    protected Object clone() throws CloneNotSupportedException {
//        return super.clone();
         Student stu = (Student)super.clone();
         // 获取学生的address
        Address address =  (Address) stu.getAddress().clone();
        // 将address对象放入 新克隆的stu中
        stu.setAddress(address);
        return stu;

    }
   }
   
   public class Address implements Cloneable{
    //联系人
    private String contectName;
    //联系电话
     private String contectPhone;
        @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
   }

测试:

 public static void main(String[] args) throws CloneNotSupportedException {
         // 创建学生对象
        Student stu1 = new Student(1001,"张三",22);
        Address address = new Address("张三的女朋友","18888888888");
        // 将收货地址 与该学生对象关联
        stu1.setAddress(address);

        //克隆一个对象
        Student stu2 = (Student)stu1.clone();

        System.out.println(stu1.getId()+"---"+stu1.getSname());
        System.out.println(stu2.getId()+"---"+stu2.getSname());
       // 问题:是否会克隆新的address对象 还是说address的内存地址相同
        System.out.println(stu1.getAddress());
        System.out.println(stu2.getAddress());

        // 对于克隆对象的引用数据类型,它默认不会创建引用数据类型的 新对象
        //  这种方式称为“浅克隆”

        // Java也可以实现深克隆
        System.out.println(stu1.getAddress().getContectName());
        System.out.println(stu2.getAddress().getContectName());
    }

结果:

1001—张三
1001—张三
com.j2008.clones.Address@4554617c
com.j2008.clones.Address@74a14482
张三的女朋友
张三的女朋友

三十五、枚举类(enum)

1、枚举类型的诞生

​ 在JDK5以前,定义常量需要使用public fiinal static… 单独定义,如果有一组常量,需要定义一组final修饰的类型。这样会很繁琐,JDK5以后推出枚举类型。 可以将一组常量定义一个自定义类,使用是通过该类型直接方法。

2、 枚举类型的语法

  public  enum 枚举类型名称{
      值1,值2,值3...
  }
  访问时: 枚举类型名称.值1 

用法1:

public enum Color {
    RED(),YELLOW(),BLUE();

}
 public static boolean isRedColor(Color color){
        if(color.equals(Color.RED)){
            return true;
        }
        return false;
    }
    
 public static void main(String[] args) {
        System.out.println(isRedColor(Color.BLUE));
        System.out.println(isRedColor(Color.RED));
}

用法2: 定义枚举类的属性 并赋值

public enum PROVINCES {
    //枚举的值  在初始化枚举值时,通过构造器给它的属性赋值
    HB("湖北",0),BJ("北京",1),HN("湖南",2),FJ("福建",3);

    //枚举的属性
    private String provinceName;
    private int index;
    //枚举的构造器
    private PROVINCES(String provinceName,int index){
        this.provinceName= provinceName;
        this.index = index;
    }

    public String getProvinceName() {
        return provinceName;
    }

    public void setProvinceName(String provinceName) {
        this.provinceName = provinceName;
    }

    public int getIndex() {
        return index;
    }

    public void setIndex(int index) {
        this.index = index;
    }
}

//输出某一枚举的值 name和 index
        System.out.println(PROVINCES.BJ.getProvinceName());
        System.out.println(PROVINCES.BJ.getIndex());

        //遍历所有的枚举的值
         PROVINCES [] provs = PROVINCES.values();
        for(PROVINCES pro :  provs){
            System.out.println(pro.getIndex()+"---"+pro.getProvinceName());
        }

三十六、Math类

java.lang.Math类用于数学计算的工具类 ,它提供都是静态方法 ,不需要构造Math对象

常用方法:

​ Math.random():获取随机数

​ Math.abs() 获取绝对值

​ Math.ceil(): 向上取整

Math.floor() :向下取整

Math.rint():取接近它的整数 ,如果两个同样接近,往偶数靠近

Math.max(int,int):返回两个数的最大值

Math.min(int,int):返回两个数的最小值

Math.round():四舍五入整数

Math .sqrt():对一个数开平方

Math.pow(double,double),对一个数的几次幂

 public static void main(String[] args) {
        System.out.println(Math.PI);
        System.out.println(Math.E);
        System.out.println(" 一个数的绝对值:"+Math.abs(-100));//100
        System.out.println(" 向上取整:"+Math.ceil(13.5)); // 14
        System.out.println(" 向上取整(比它大的最近数):"+Math.ceil(-13.5));// -13
        System.out.println("向下取整:"+ Math.floor(13.5));// 13
        System.out.println("向下取整"+Math.floor(-20.2));// -21

        //四舍五入
        System.out.println(Math.round(23.4));// 23
        System.out.println(Math.round(-23.5));//-23
        System.out.println(Math.round(-24.5));//-24

        System.out.println(Math.rint(23.4));// 23
        System.out.println(Math.rint(23.5));//24 如果两个一样接近,取接近偶数
        System.out.println(Math.rint(22.5));//22
        System.out.println(Math.rint(0.6));//0

    }

三十七、大数据类型BigDecimal

1、BigDecimal类

两个double类型的计算可能导致精度不准确,这里使用

java.math.*里面提供了BigDecimal类(提供高精度计算的方法)

public static void main(String[] args) {
         double a= 1.200000;
         double b= 1.35433;
         double c = a+b;
        System.out.println(c);
        System.out.println(0.05+0.01);
        System.out.println(1.0-0.42);  // 会出现精度问题  计算不准确

        // 使用BigDecimal ,先将类型转成字符串 (为了避免精度问题)
        BigDecimal num1 = new BigDecimal("0.051");
        BigDecimal num2 = new BigDecimal("0.012");
        // 两个数相加

        BigDecimal sum = num1.add(num2 ) ;
        // 设置保留2位整数  四舍五入
        sum =sum.setScale(2,BigDecimal.ROUND_HALF_UP);
        System.out.println(sum);

        // 减法
       sum =  num1.subtract(num2);
        System.out.println(sum);

       // 乘法
       sum =  num1.multiply(num2);
        System.out.println(sum);

       // 除法
       sum = num1.divide(num2,2,BigDecimal.ROUND_HALF_UP);
        System.out.println(sum);

    }

2、NumberFormat类

java.text.NumberFormat类 :用于将数值格式转成指定格式并输出字符串形式的类 。

DecimalFormat类: 属于NumberFormat的子类。

  // NumberFormat类是对数值类型的格式化类,其中 DecimalFormat是继承NumberFormat
        // 获取数值的货币表现形式
        NumberFormat nf = NumberFormat.getCurrencyInstance();
        String s1 = nf.format(23424.2);
        System.out.println(s1);

        //获取数值的百分比
        nf = NumberFormat.getPercentInstance();
        s1= nf.format(0.654);
        System.out.println(s1);

        //根据指定的格式匹配百分比
        nf = new DecimalFormat("##.##%");
        s1=nf.format(0.654);
        System.out.println(s1);

        // 根据指定的模式匹配千分比
        nf = new DecimalFormat("##.##\u2030");
        s1 = nf.format(0.6543);
        System.out.println(s1);

        // 根据指定的模式 转成科学计数法
        nf = new DecimalFormat("#.###E0");
        s1 = nf.format(198200);
        System.out.println(s1);

        // 根据指定的模式  将字符串转成 指定格式数值型
        String s2 ="25.3%";
        nf = new DecimalFormat("##.##%");
        Number dd = nf.parse(s2);
        double num = (double)dd;
        System.out.println("这个字符串转成double:"+num);


题目: 控制台输入 n个学生 的分数 , 60分以上的是及格,85分以上的是优秀,问学生的及格率是多少,优秀率是多少,使用百分比表示。

三十八、 Java的util包

一、日期和日历类

1、日期类 Date

​ 在Java中用于表示日期的类 java.util.Date() ,用于获取日期和时间的对象, 不过这个类的一些方法以及过时(被日历类取代)

​ 创建日期类

 Date  date = new Date();
 Date date = new Date(long) ;
        // 创建日期类的对象
        Date date = new Date();
        //获取当前时间 以 标准格式
        System.out.println(date);
        // 获取当前时间的毫秒数
        System.out.println(date.getTime());
        //通过毫秒数构建当前时区的 时间
        Date date2 = new Date(100000);
        System.out.println(date2);

        // 获取 纪元时间  相隔本地时间8时区
        System.out.println(date.toGMTString());
        System.out.println(date.toLocaleString());

        //测试时间的先后
        System.out.println(date.after(date2));// true
        System.out.println(date.before(date2));//false
        System.out.println(date.compareTo(date2));//比较时间大小
        // 时间转成字符串
        System.out.println(date.toString());

2、日历类 Calendar

java.util.Calendar 是表示日历类, 它是一个单例模式 ,通过 getInstance()获取一个日历对象, 并获取日历的任意时间,日期。

常用方法

​ getInstance() : 获取日历对象

​ get() :获取指定的单位的日历数值 (年,月,日 等等)

​ set():设置指定单位的日历数值

​ add() :添加指定单位的日历数值

​ getTimeInMills() :获取日期的毫秒数

      // 获取当前日历对象
        Calendar cal =  Calendar.getInstance();
        // 日历的属性 (fields)
        System.out.println(Calendar.DATE);
        System.out.println(Calendar.YEAR);
        System.out.println(Calendar.MONTH);
        //获取当前日历的年份
        System.out.println(cal.get(Calendar.YEAR));
        // 获取月份 (返回 0-11)
        System.out.println(cal.get(Calendar.MONTH));
        // 获取日(1-31)
        System.out.println(cal.get(Calendar.DATE));
        // 获取日期
        System.out.println("获取当前日历的日期:"+ cal.getTime());
        // 添加日历 (指定的field 和 数量)
        // 将当前日历添加2个月
        cal.add(Calendar.MONTH ,2);
        System.out.println("新的日期:"+cal.getTime());
        cal.add(Calendar.DATE,-3);
        System.out.println("新的日期:"+cal.getTime());

        // 判断日期的前后 after  before
        // 题目 给定两个日期, 计算相差多少天
        Calendar cal1 = Calendar.getInstance();

计算两个日历(日期)的相差天数/月数

 // 重写构建一个时间
        Calendar cal2 =  Calendar.getInstance();
        // 设置指定的时间
        cal2.set(2020,10,20);
        System.out.println("cal2---"+cal2.getTime());
        // 原始方式  通过毫秒数计算
        long  n =  cal1.getTimeInMillis()-cal2.getTimeInMillis();
        System.out.println("n---->"+n);
        long days =  n/(1000*60*60*24);
        System.out.println("days:"+days);

        // java8 的方式
        LocalDate date1 =   LocalDate.of(2020,2,22);
        LocalDate date2 =   LocalDate.of(2020,3,22);
        long days2 = date1.until(date2,ChronoUnit.DAYS);
        System.out.println("两个时间相差的天数:"+ days2);

3、字符串与日期格式的转换

  /**
     * 字符串转日期
     * @param str
     */
    public static Date strToDate(String str) throws ParseException {
        //使用格式化日期类    指定格式 :yyyy-MM-dd HH:mm:ss
        SimpleDateFormat  sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
         // 指定格式的字符串 转成 日期
        return sdf.parse(str);

    }
/**
     * 日期转指定格式的字符串
     */
    public static String dateToString(Date date){
        // 使用同样的方式
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
        return sdf.format(date);
    }
       // 调用转换方法
        String s = "2020-10-22 10:10:10";
        Date myDate= strToDate(s); // 注意 这里的格式需要匹配yyyy-MM-dd HH:mm:ss
        System.out.println(myDate);

        // 将日期转成字符串
        System.out.println(dateToString(myDate));

二、正则表达式

1、正则表达式定义

​ 正则表达式(Regular Expression)由字母和符号组成的具有特定意义的公式,用于匹配或检索符合条件的字符串。

​ 例如 在网页上输入用户名,要求用户名必须由数字,字母,下划线,或者长度必须8-16位之间 像这样的满足条件的公式,都是编写的 正则表达式进行验证。

^[a-zA-Z0-9_]{8,16}$

解释: ^ :表示以指定的字符开头

      $:以指定符合结尾

[ a-zA-Z]: 字符必须是这个a-z或 A-Z之间的一个字符

​ {8,16}:匹配前面的字符8次到16次之间

正则表达式不属于某一门编程语言,可以在很多语言中使用, 例如 Java ,Python 、JS、MySql

Java提供对正则表达式的支持,有如下类

a、java.util.regex.Pattern 正则表达式的编译器类

b、java.util.regex.Matcher 正则表达式的匹配器

c、java.lang.String 中的方法自动兼容 正则语法

1、 元字符

元字符是组成正则表达式的最基本的符号 ,通常代表一定意义

元字符解释
.匹配任意一个字符
\w匹配一个数字,字母,_ 或汉字 \W :对\w取反
\d匹配一个数字 \D:对\d取反
\s匹配一个空白字符 \S:对\s取反
\b匹配以什么字符开头
^以指定的字符串开头 ,用于正则开始的标志位
$以指定的字符串结尾,用于正则结束的标志位
  1. 匹配以abc开头的字符串
   ^abc  或者  \babc

2、匹配8位数的QQ号码

^\d\d\d\d\d\d\d\d$  或  ^\d{8}$

3、匹配以1开头的手机号

^1\d\d\d\d\d\d\d\d\d\d$
 String s ="abc";
        System.out.println(s.matches("abc"));
        System.out.println(s.matches("^abc"));
        s="6";
        System.out.println(s.matches("\\d"));
        s="123456780";
        System.out.println(s.matches("\\d{8}"));
区间段
   //[x] :表示匹配一个字符
        //[abc]:表示匹配a或b或c 一个字符
        System.out.println("a".matches("[abc]"));
        System.out.println("b".matches("[abc]"));
        //[^abc]:匹配不是 a 或b 或c
        System.out.println("b".matches("[^abc]"));
        // [0-9]:匹配任意一个数字
        System.out.println("4".matches("[0-9]"));
重复限定符

正则表达式中用于匹配重复次数的符号

重复限定符意义
*匹配前一个字符0次或多次
匹配前一个字符0次或1次
+匹配前一个字符1次或多次
{n}匹配前一个字符n次
{n,}匹配前一个字符至少n次
{n,m}匹配前一个字符n到m次(包含n次,m次)
转义

如果要匹配的字符串中本身就包含小括号,那是不是冲突?应该怎么办?
针对这种情况,正则提供了转义的方式,也就是要把这些元字符、限定符或者关键字转义成普通的字符,做法很简 答,就是在要转义的字符前面加个斜杠,也就是\即可。 如:要匹配以(ab)开头:

或条件

回到我们刚才的手机号匹配,我们都知道:国内号码都来自三大网,它们都有属于自己的号段,比如联通有 130/131/132/155/156/185/186/145/176等号段,假如让我们匹配一个联通的号码,那按照我们目前所学到的正 则,应该无从下手的,因为这里包含了一些并列的条件,也就是“或”,那么在正则中是如何表示“或”的呢? 正则用符号 | 来表示或,也叫做分支条件,当满足正则里的分支条件的任何一种条件时,都会当成是匹配成 功。
那么我们就可以用或条件来处理这个问题

 // 创建匹配格式的编译器
        String phone ="13388880000";
        Pattern pattern = Pattern.compile("1[356789][0-9]{9}");
        //  根据编译器获取匹配器
        Matcher matcher = pattern.matcher(phone);
        System.out.println("是否匹配目标字符串:"+matcher.matches());

        // 或者简写  匹配年龄 (0-100   01-09 | 10-99 |100)
       boolean flag =  Pattern.matches
               ("(0?[0-9])|([1-9][0-9])|(100)","10");

        System.out.println(flag);

        // 匹配一个特殊字符
        // 匹配一个字符串中是否包含 .
        String s2 ="adb";
        System.out.println(s2.matches(".*b.*"));
        // 因为.表示的所有字符  当匹配.时,需要转义
        System.out.println(s2.matches(".*\\..*"));

        // 对于特殊符号 需要转义   \\
        // 匹配邮箱   包含  任意字符任意个数@域名.com
        //    .cn  .com.cn  .gov  .net
        String email="1@softeem.com";
        System.out.println(email.matches("\\w+@\\w+(\\.[a-z]{2,3}){1,2}"));

        // 匹配一级网址 http://    https://d
        String url="http://www.baidu.com";
        System.out.println(url.matches("https?://www\\.\\w+\\.[a-z]{2,3}"));


        // 匹配生日格式   1900-13-01  ~ 2099-12-31   01  -31
        String birthday="1998-13-10"; // yyyy-MM-dd
        String regexp="((19)|(20))[0-9]{2}-((0[0-9])|(1[0-2]))-((0[1-9])|([1-2][0-9])|(3[0-1]))";
        System.out.println(birthday.matches(regexp));

匹配汉字

   // 匹配汉字
        String uname ="张";
        System.out.println(uname.matches("[\\u4E00-\\u9FFF]+"));



三、定时器类(Timer类)

1、Timer类

​ java.util.Timer类 是一个任务调度的定时器类,可以安排任务一次执行,或定期重复执行

​ 创建Timer对象

 Timer timer = new Timer();
 Timer timer = new Timer(name);

​ 常用方法

timer.schedule(TimerTask,2000); 2秒后执行一次任务

timer.schedule(TimerTask,2000,1000); 2秒后开始执行任务,每1s执行一次

2、TimerTask类

​ java.util.TimerTask类是由定时器执行的任务类,是一个抽象类。

import java.util.Timer;
import java.util.TimerTask;

/**
 * ClassName: TestTimer
 * Description:
 * date: 2020/10/22 17:10
 *
 * @author wuyafeng
 * @version 1.0   softeem.com
 */
public class TestTimer {
    //统计打印次数
    static int count=0;
    // 创建一个定时器类
    static Timer timer = new Timer("我的定时器");

    public static void main(String[] args) {

        // 开始定时一个任务
        TestTimer obj = new TestTimer();
        // 多少毫秒之和 执行一次任务
      //  timer.schedule(obj.new MyTask(),2000);
        // 2秒之后开始执行,每隔1s执行一次
        timer.schedule(obj.new MyTask(),2000,1000);



    }
    // 定义内部类时,可以直接使用外部类的属性  和它的对象
    class MyTask extends TimerTask{

        @Override
        public void run() {
            System.out.println("你好!!!");
            count++;

            if(count>=10){
                System.out.println("停止");
                //停止
                timer.cancel();
            }
        }
    }
}

三十九、泛型

1、泛型定义

​ 泛型(generics)是JDK5.0以后的特性,提供了编译期间安全监测机制,它是将数据类型参数化的一种方式。 例如:在对方法进行编写参数列表时,以前我们需要知道方法的参数类型 ,现在使用泛型机制可以将方法的参数类型也作为 “未知的类型” ,在调用该方法时传递该类型。

2、泛型的使用

2.1 泛型类(generic class)

​ 它是一种具有一个或多个类型变量的类,(一个变量可以有多种类型)

​ 语法

  public  class  类<T>{
      // 类里面的数据类型 和 方法返回值,以及方法的参数都可以使用T  
      // <>里面可以是任意大写字母
      
  }
public class People<T> {
    private T name;
    private T sex;

    public T getName() {
        return name;
    }
       public People(T name,T sex){
        this.name= name;
        this.sex = sex;
    }
    public People(){

    }
}
       // 创建没有指定泛型的对象 ,它默认是Object类型
        People  obj= new People();
        obj.setName("李四");
        System.out.println(obj);
        System.out.println(((String)obj.getName()).length());

        // 创建泛型类的对象
        People<String> people = new People<String>("张三","男");
        System.out.println(people);
        System.out.println(people.getName().length());

定义泛型的字母

T : Type: 变量类型

K: Key : 任意键的类型

V: Value : 任意值的类型

E:ELement 用于定义集合的元素类型

2.2 泛型接口(generic interface)

​ 在接口中定义泛型,使接口的方法可以使用该泛型,实现类实现该接口时需要指定接口的类型、

语法:

public interface Genarator<T> {
    public T getValue();

    public void setValue(T s);


}
public class StringGenarator implements  Genarator<String> {
   private String name;

    @Override
    public String getValue() {
        return name;
    }

    @Override
    public void setValue(String s) {
        this.name=s;
    }

public class StudentGenarator implements Genarator<Student> {
    private Student  stu;

    @Override
    public Student getValue() {
        return stu;
    }

    @Override
    public void setValue(Student s) {
        this.stu = s;
    }
}

泛型接口的好处:

​ 让接口的方法的返回值或参数类型 也参数化 (泛型)

2.3 泛型方法

​ a、为什么会使用泛型方法

​ 当一个类中 只有某个方法需要使用泛型,而不是类的全部方法使用泛型,这时可以将泛型定义的范围缩小,通常我们可以定义进行泛型方法。

b、定义泛型方法

​ 语法:

  public  class 普通类{
      
      public <T>  T  getValue(){
          
      }
      
      public <T> void setValue(T t){
          
      }  
      
  }
public class Convert {
    /**
     * 转成字符串的方法
     * @param <T> : 任意类型

     * @return
     */
    public <T> String convertString(T t){
        return  t.toString();
    }

    public <K,V> V converted(K k){
        return (V)k;// 强转的前提 是k -v 有关系
    }

泛型的好处:

​ 1、 可以对类的数据类型 写通用类型,提高代码的复用性 和 可扩展性

2.4 泛型通配符

​ 在定义泛型时除了可使用大写字母表示一种泛型类以外,还可以使用通配符表示泛型类型,如下三种表示方法

<?> :表示一种通用的泛型类,与相似 <? extends T> :表示 泛型类型是T的子类,或者是T <? super T> : 表示泛型类型是T的父类,或者是T 问题: <?> 与 的区别
  T t = new T() // 语法满足
  ? t = new ?()  // 语法不满足

​ 是一种确定的类型 , 可以表示定义泛型类或泛型方法

<?> 是一种不确定的类型, 不能定义泛型类或泛型方法, 通常用于作为方法的形参
public class Dept<T> {
    // 第一个员工
     private T first;
     // 第二个员工
     private T second;
public class Employee {
    private String ename;

    public String getEname() {
        return ename;
    }

public class Manager extends  Employee {

    // 通过经理对象 给经理赋值名称
    public Manager(String ename){
        super(ename);
    }

// 使用不确定的泛型类型 <?>

    /**
     *
     * 这里的部门的泛型可以是任意类型
     */
    public void showInfo(Dept<?> dept){

        System.out.println(dept.getFirst());

    }

    /**
     * @param dept 的泛型可以是Employee  或者继承自Employee
     * @param dept
     */
    public void showInfo2(Dept<? extends Employee> dept){

        System.out.println(dept.getFirst());
        System.out.println(dept.getSecond());

    }

    /**
     *
     * @param dept 的泛型必须是 Manager 或者 Manager的父类
     */
    public void showInfo3(Dept<? super Manager> dept){

        System.out.println(dept.getFirst());
        System.out.println(dept.getSecond());

    }

    public static void main(String[] args) {
         TestDept obj = new TestDept();

         //创建部门对象
        Dept<String> dept = new Dept();
        dept.setFirst("员工1");
        dept.setSecond("员工2");
         obj.showInfo(dept);

         // 在部门中添加 员工对象
        Dept<Employee> dept2 = new Dept();
        dept2.setFirst(new Employee("小强"));
        dept2.setSecond(new Employee("小花"));
        //这里的dept2的泛型是 Employee
        obj.showInfo2(dept2);

        Dept<Manager> dept3 = new Dept();
        dept3.setFirst(new Manager("张经理"));
        dept3.setSecond(new Manager("王经理"));
        //这里的dept3的泛型是  Manager
        obj.showInfo2(dept3);

        //  调用时 参数的泛型必须是 Manager 或Manager的父类
        obj.showInfo3(dept3);
        obj.showInfo3(dept2);
    }

三十九、集合框架

1、为什么会有集合?

存储多个元素我们以前学过数组类型, 由于数组类型特点是 相同类型且长度固定 ,如果需要存储某一天的新闻数据,用数组不合理 ,无法确定当天数量。 Java中提供可变长度的存储多个元素的数据类型,还可以存储不同数据结构的数据。这样的类型 就是“集合类型”

​ 数组和集合的区别?

​ a、数组的长度固定,集合的长度可自动扩容

​ b、数组的数据类型固定,集合可以存储任意类型 ,集合可以支持泛型

​ c、数组没有方法,而集合提供大量的方法

​ d、Java中提供一个动态数组 集合类型,或其他结合类型

2、集合的分布图

在这里插入图片描述

2.1 集合的顶级接口: Collection

​ Collection属于单列集合的根接口,它扩展的主要子接口包括 java.util.List 和 java.util.Set接口,

List接口特点存储有序 且 可重复的元素, 而Set接口特点存储无序且不可重复的元素,其中List下扩展常用的实现类包括 java.util.ArrayList 和java.util.LinkedList 和Vector , 其中Set接口下扩展的实现类包括 java.util.HashSet 和 java.util.TreeSet .

​ 集合接口的常用方法:

  • public void add(E) : 把给定的元素添加到集合中

  • public void clear():清空集合中的所有元素

  • public boolean remove(E):删除集合中指定的元素,删除成功返回true

  • public boolean contains(E):判断该元素是否存在集合中

  • public boolean isEmpty():判断是否为空集合对象 null会报异常

  • public int size():获取几个元素的大小

  • publict Object toArray() : 将集合元素转成对象数组

    public static void main(String[] args) {
           //通过接口创建实现类 , 可指定存储的泛型
  
          Collection<String> collection = new ArrayList<String>();
          // 集合中指定了元素的类型  String
          collection.add("hello");  // 默认添加到末尾
          collection.add("hi");
          collection.add("哈哈");
          System.out.println("元素大小:"+ collection.size());
          // 删除集合元素  (后面的原始往前 移动)
          collection.remove("hi");
          System.out.println("元素大小:"+collection.size());
          System.out.println("第一个元素:"+((ArrayList<String>) collection).get(0));
          System.out.println("第二个元素:"+((ArrayList<String>) collection).get(1));
  
          // 判断元素是否存在
          System.out.println("是否存在哈哈:"+collection.contains("哈哈"));
          // 转成数组对象
          Object [] objs = collection.toArray();
          //遍历元素
          for(Object obj : objs){
              System.out.println("数组的元素:"+obj);
          }
          //清空元素  clear
          collection.clear();
          // 大小
          System.out.println("清空后元素的大小(对象依然存在,只能内容为空)"
                  +collection.size());
          // 判断对象中是否是空集合
          System.out.println(collection.isEmpty());
  
        
      }

Iterator 集合遍历接口

    // 直接对集合元素遍历   泛型只能是包装类
          Collection<Integer> scores = new ArrayList<>();
          scores.add(90);
          scores.add(88);
          scores.add(92);
          scores.add(91);
          //遍历集合  使用   ,
          // 再遍历集合时 不能一边遍历集合一边删除集合元素,这样会改变它的遍历的模式
          Iterator<Integer> is = scores.iterator();
          //判断是否有下一个元素   ,如果true ,则可以通过next获取值 
           while(is.hasNext()){
               Integer score = is.next();
               System.out.println(score);
           }
  

2.2 ArrayList类:

​ java.util.ArrayList是一个数组结构的集合,实现动态数组的功能,扩展所有Collection的方法

数组结构的本质: 线性结构的顺序结构,ArrayList中使用连续的内存空间存储, 访问时通过下标(元素所在的位置)访问

ArrayList的数据结构

分析一个类的时候,数据结构往往是它的灵魂所在,理解底层的数据结构其实就理解了该类的实现思路,具体的实现细节再具体分析。

ArrayList的数据结构是:

img

说明:底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据。我们对ArrayList类的实例的所有的操作底层都是基于数组的。

​ 源码分析参考:https://www.cnblogs.com/zhangyinhua/p/7687377.html#_lab2_0_1

public static void main(String[] args) {
         //  通过ArrayList 创建集合对象
        // 还可以存储自定义对象  默认容量是 10
        ArrayList<String> list = new ArrayList<String>();
        // 存储有序集合
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        // 将元素插入到指定位置
        list.add(1,"ddd");
        //直接 遍历元素  get(Index) 通过下标访问元素
        for(int i = 0 ;i<list.size();i++){
            System.out.println("集合元素:"+ list.get(i));
        }
        // 设置集合的最小容量
        list.ensureCapacity(20);



    }

集合框架

四十

有关LinkedList的集合的,它是一个链表结构的集合

1、链表结构

1.1 单链表的结构

所谓单链表(Linked)在内存中不连续的一端内存空间, 链表的每一个元素是一个节点,每一个结点由数据元素和下一个结点的存储位置组成,链表结构与数组结构最大区别是链接结构的存储内存是不连续的,而数组结构的内存是连续的,链表结构不能与数组结构一样快速查找,

​ 链表结构操作特点是 添加,删除元素效率高,查询效率低;

​ 数组结构操作特点: 添加,删除效率低,查询效率高

​ 链表结构的示意图

img

前驱: 该节点的上一个元素的地址

后继: 该节点的下一个元素的地址

链表结构中最后一个元素的”后继“为null

1.2 单链表的实现

链表实现添加元素:

  /**
     * 添加到最后元素
     * @param obj
     */
     public void addLast(Object obj){
         //将节点添加到最后
         //add(obj , this.size);
         // 创建节点
//         Node node = new Node(obj);
//         // 找到最后一个元素的地址
//         Node lastNode = this.header;
//         for(int i = 0;i<this.size-1 ;i++){
//             lastNode  = lastNode.next;
//         }
//
//           lastNode.next=node;
         // 找最后一个结点 (由于最后一个结点的next是null)
         Node node = new Node(obj);
           Node lastNode = this.header;
           while(lastNode.next!=null){
               lastNode = lastNode.next;
           }
           lastNode.next = node;

           this.size++;

     }

在这里插入图片描述

  /**
     * 删除第一个节点
     * @param index
     * @return
     */
    public void removeFirst(){
        //删除第一个节点
        if(this.size==0){
            throw new IllegalArgumentException("没有需要删除的原始");
        }
        //  获取当前连接的“后继”
        Node node =  this.header.next;
        // 并让后继作为头
        this.header = node;
        this.size--;
    }

    /**
     * 删除最后节点
     */
    public void removeLast(){
        //删除是否存在数据
        if(this.size==0){
            throw new IllegalArgumentException("没有需要删除的原始");
        }
        // 找最后一个元素的前一个 地址   ,并将该地址的next 改为null
        Node cur  = this.header;
        Node pre = this.header;
        while(cur.next!=null){
               pre = cur;
                // 下一个变为当前
               cur =  cur.next;
        }
         // 最后一个元素  就是 当前
        pre.next = null;
        size--;
    }

2、队列结构

队列结构(Queue): 在基于链表结构的基础上 ,实现的一种“先进先出”的结构, 常用操作 入队(put),出队(pop) ,设置队列的头结点 和 尾结点
在这里插入图片描述

package com.j2008.dataStruct;

/**
 * ClassName: MyQueue
 * Description:
 * date: 2020/10/26 16:41
 *
 * @author wuyafeng
 * @version 1.0   softeem.com
 */
public class MyQueue<T> {
    // 头结点
    private Node front;
    //  尾结点
    private Node tail;
    // 大小
    private int size;

    public MyQueue(){
        //  头,尾为空
        this.front= this.tail=null;
    }

    class Node{
        private T obj;
        private Node next;
        public Node(T obj){
            this.obj = obj;
        }

        public T getObj() {
            return obj;
        }

        public void setObj(T obj) {
            this.obj = obj;
        }

        public Node getNext() {
            return next;
        }

        public void setNext(Node next) {
            this.next = next;
        }
    }

    /**
     * 入队 : 将元素添加到队列的尾部
     */
    public void put(T obj){
        // 创建节点
        Node node = new Node(obj);
        // 如果元素为空 则头就尾,尾就是头
        if(isEmpty()){
            this.front = this.tail = node;
            return ;
        }
        // 将新元素的地址 作为尾的next
        this.tail.next=node;
        //将新元素的结点 作为尾节点
        this.tail = node;

        this.size++;
    }

    /**
     * 出队: 将元素从队列的头部移除 (保持与队列脱离关系)
     * @return
     */
    public T pop(){
        if(isEmpty()){
            throw new IllegalArgumentException("没有弹出的原始");
        }
        // 移除头部元素
        Node popNode = this.front;
        // 设置现在的头元素是下一个
        this.front = popNode.next;
        //  将弹出的元素next 设置null,与队列脱离关系
        popNode.next=null;
        this.size--;
        // 如果没有元素了 则需要 设置头尾都是null
        if(this.size<0){
            this.front=this.tail=null;
        }
        return  popNode.getObj();
    }

    /**
     * 判断元素是否为空
     * @return
     */
    public boolean isEmpty(){
        if(this.front==null && this.tail==null){
            return true;
        }
        return false;
    }

}

3、栈结构

栈(Stack)结构也是常用数据结构之一,它具有 “后进先出” 的特点

在这里插入图片描述

public class MyStack<T> {
    // 定义一个数组 ,用于存储元素
    private Object[] obj;
    private int size;
    public MyStack(){
        obj = new Object[10];
        size=0;
    }

    /**
     * 入栈: 压入栈顶元素
     * @param t
     */
    public void push(T t){
        expandCapacity(size+1);
        obj[size]=t;
        size++;
    }

    /**
     *  返回栈顶元素:peek
     */
    public T peek(){

        if(size>0) {
            return (T) obj[size - 1];
        }
        return  null;
    }

    /**
     * 出栈: 返回栈顶的元素,并删除该元素
     * @return
     */
    public T pop(){
        T t = peek();
        if(size>0) {
            // 将最后一个元素 删除
            obj[size - 1] = null;
            size--;
        }
        return t;
    }

    /**
     * 是否为空元素
     * @return
     */
    public boolean  isEmpty(){
        return  size==0;
    }
    /**
     * 扩容数组大小 : 扩容1.5倍
     */
    public void expandCapacity(int size){
        if(obj.length< size){
            // 需要扩容
            int length = size*3/2 + 1;
           this.obj =  Arrays.copyOf(this.obj,length);
        }
    }

}

四十一、LinkedList集合

java.util.LinkedList集合是java.util.List的实现类,实现List接口的所有方法(添加,删除,查找,判断是空等) ,它添加,删除元素较快,查询相对慢,但是查询头尾元素较快

LinkedList集合实现双向链表接口,实现从头元素到尾元素的链表和从尾到头元素的链表,目标为了增加元素的检索效率 ,如下图

在这里插入图片描述

关于LinkedList实现大量操作头元素和尾元素的方法。 其中必须通过LinkedList的引用创建该对象

public void addFirst(E e) :将指定元素插入此列表的开头。

public void addLast(E e) :将指定元素添加到此列表的结尾。

public E getFirst() :返回此列表的第一个元素。

public E getLast() :返回此列表的后一个元素。

public E removeFirst() :移除并返回此列表的第一个元素。

public E removeLast() :移除并返回此列表的后一个元素。

public E pop() :从此列表所表示的堆栈处弹出一个元素。

public void push(E e) :将元素推入此列表所表示的堆栈。

public boolean isEmpty() :如果列表不包含元素,则返回true

四十二、 Set集合

​ java.util.Set 接口 继承自Collection接口,实现对元素的基本操作 ,与java.util.List区别于 Set集合存储无序,且唯一的元素,List存储有序,且可重复的元素

​ Set接口的实现类 HashSet 、 LinekedHashSet 、TreeSet

1、HashSet

​ HashSet集合依据元素的哈希值确定在内存中的存储位置, 所谓Hash值是内存中哈希表的唯一标志,通过哈希值可快速检索到元素所在的位置 , 所以它查询效率高 ,与HashSet类似结构的包括HashMap 等

​ 创建一个HashSet时,就是创建一个HasMap( 关于HashMap结构后面讲)

​ 什么是哈希表?

在Java1.8以前,哈希表的底层实现采用数组+链表结构,但是这样对于“Hash冲突” (两个对象生成的哈希值一样),即多个元素存储在一个“数据桶”中, 这样查找该元素时,依然效率低下, 为了解决由于哈希冲突导致的数据查询效率低下,JDK8以后将哈希表实现采用 数组+链表+红黑树结构

在这里插入图片描述

1.2.HashSet存储自定义对象类型

​ HashSet对于对象是否相同的依据,判断对象的hashCode值和equals是否相等,如果它们相等则判断元素一致,不能重复添加

public class People {
    private int pid;
    private String pname;
    private int age;

    public People(int pid, String pname, int age) {
        this.pid = pid;
        this.pname = pname;
        this.age = age;
    }

    public int getPid() {
        return pid;
    }

    public void setPid(int pid) {
        this.pid = pid;
    }

    public String getPname() {
        return pname;
    }

    public void setPname(String pname) {
        this.pname = pname;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public int hashCode() {
        return this.pid;
    }

    @Override
    public boolean equals(Object obj) {
         if(this == obj){
             return true;
         }
         if(obj instanceof  People){
             People p = (People) obj;
             if(p.pid == this.pid && p.getPname().equals(p.getPname())){
                 return true;
             }
         }
         return false;
    }
}
   //存储 对象类型
        Set<People> sets = new HashSet<>();
        People p = new People(1001,"关羽",100);
        People p2 = new People(1002,"张飞",100);
        People p3 = p2;
        System.out.println("p的hashcode:"+p.hashCode());
        sets.add(p);
        // 检查是否为同一个地址
        sets.add(p);

        sets.add(p2);
        sets.add(p3);


        // 插入一个重新new的张飞对象  HashSet以 equals和hashcode的结果作为是否重复对象的依据
        People p4 = new People(1002,"张飞",90);
        sets.add(p4);  //  会当做是重复的对象 ,不能添加成功。

        System.out.println("sets的长度:"+sets.size());
        for(People obj : sets){
            System.out.println(obj.getPid()+"---"+obj.getPname()+"---"+obj.getAge());
        }

1.3 LinkedHashSet

​ 在HashSet中存储的数据是唯一且无序,如何保证数据的有序型,可通过扩展HashSet的子类完成,

java.util.LinkedHashSet ,它实现有序的Hash结构, 它的底层实现使用链表+哈希结构

创建LinkedHashSet时,就是创建一个LinkedHashMap结构 ,linkeHashSet中如何保证顺序一致性

​ accessOrder = false; 按照插入的顺序存储 accessOrder = true: 按照访问的顺序存储。

        // 创建LinkedHashSet对象
        LinkedHashSet<String> set = new LinkedHashSet();
        set.add("aaa");
        set.add("bbb");
        set.add("ccc");
        set.add("ddd");
        //遍历元素
        for(String s : set){
            System.out.println(s);
        }

1.4 TreeSet

TreeSet实现对Set元素的排序功能, 也包含基础的Set集合功能。 存放在TreeSet中的元素时有序的,默认升序,也可以自定义排序规则。

两种方式实现自定义排序规则

1、对元素(自定义类)实现 java.lang.Comparable 接口,重写 compareTo方法

public class Fruit  implements  Comparable<Fruit>{
    private  int id;
    private String name;
    public int compareTo(Fruit o) {
            // return this.id-o.id;  升序
        return  o.id - this.id;
            // 正数: 前一个大于后一个
            // 负数: 前一个小于后一个
    }
 }
        // 实现自定义排序规则的方式一 :  对象实现Comparable接口 (java.lang)
        // 重写compareTo 方法。
        TreeSet<Fruit> fruitSet = new TreeSet<>();
        Fruit f1 = new Fruit(100,"苹果");
        Fruit f2 = new Fruit(101,"香蕉");
        fruitSet.add(f1);
        fruitSet.add(f2);
        System.out.println(fruitSet.size());
        for(Fruit f : fruitSet){
            System.out.println(f.getId()+"---"+f.getName());
        }

2、通过匿名内部类的方式 在创建TreeSet时,创建自定义排序规则 ,new Comparator的接口

  // 自定义排序规则的方式二: 对treeSet实现匿名内部类  new Comparator(java.util)
        TreeSet<Integer> scores  = new TreeSet (new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2-o1;  //降序
            }
        });
        //  添加元素
        scores.add(80);
        scores.add(87);
        scores.add(90);
        scores.add(78);
        for(Integer score :  scores){
            System.out.println(score);
        }

  // 按照对象的某一属性降序
        TreeSet<People> peopleSet = new TreeSet<>(new Comparator<People>() {
            @Override
            public int compare(People o1, People o2) {
                return o2.getAge()- o1.getAge(); // 根据age降序排列
            }
        });
        peopleSet.add(new People(1001,"张飞",100));
        peopleSet.add(new People(1002,"刘备",102));
        peopleSet.add(new People(1003,"关羽",108));

        for(People p : peopleSet){
            System.out.println(p.getPid()+"--"+p.getAge());
        }

四十三、Map集合

java.util.Map集合用于存储key-value的数据结构 ,一个键对应一个值,其中键在集合中是唯一的, Value可以重复, 例如 学号与学生的关系,省份编号对应省份信息, 对于Map集合的常用实现类包括 HashMap 、LinkedHashMap、HashTable 、TreeMap 等 。

1、HashMap(重点)

​ java.util.HashMap 存储无序的,键值对数据,HashMap的实现原理在JDK1.8以前使用 链表+数组结构,1.8以后使用链表+数组+红黑树结构, 使用Hash表的存储方式其检索效果高

​ 特点:

​ a、HashMap的key唯一,且无序,value不唯一

​ b、HashMap的key和value 都可以为null

​ c、对于相同key 元素,它的value会覆盖原始value

​ HashMap的常用方法

HashMap() 
构造一个空的 HashMap ,默认初始容量(16)和默认负载系数(0.75)。  
HashMap(int initialCapacity) 
构造一个空的 HashMap具有指定的初始容量和默认负载因子(0.75)。  
HashMap(int initialCapacity, float loadFactor) 
构造一个空的 HashMap具有指定的初始容量和负载因子。  
HashMap(Map<? extends K,? extends V> m) 
构造一个新的 HashMap与指定的相同的映射 Map 。  

​ a、put(K key,V value) : 存储key-value 到容器中

​ b、V get(K key): 根据key 获取对应的value

​ c、Set keySet(): 返回所有的key,Set集合

​ d、boolean containsKey(K key): 判断key是否存在

​ e、clear():清空容器的原始

​ f、boolean containsValue(V value):判断value是否存在

​ g、Collection values() : 返回所有的value集合

​ h、isEmpty(): 判断是否为空集合

​ i、remove(Object key) : 根据key删除这个key-value

​ j、size():返回元素的大小

​ k、Set<Map.Entry<Key,Value>> entrySet(): 返回容器的key-value的实体类的集合,方便遍历元素

HashMap的源码分析

   // 创建HashMap对象
        Map<String , Integer> cards = new HashMap();
        //存储
        cards.put("红桃",3);
        cards.put("黑桃",3);
        cards.put("方片",2);
        cards.put("梅花",8);
        cards.put("红桃",2); // 会覆盖原始的value
         //获取指定key的value元素
        System.out.println(cards.get("红桃"));
        // 获取所有的key
        Set<String> keys= cards.keySet();
        //通过遍历所有的key 访问对应的value
        for(String k : keys){
            System.out.println(k+"-----"+cards.get(k));
        }
        // 判断key 是否存在, 判断value是否存在
        System.out.println("是否有红桃:"+cards.containsKey("红桃"));
        System.out.println("判断是否有value:"+cards.containsValue(2));
        // 获取所有的value
        Collection<Integer> values =  cards.values();
        Iterator its= values.iterator();
        while(its.hasNext()){
            System.out.println("values ----"+its.next());
        }

        //获取所有的元素
        System.out.println(cards.size());

        // 遍历map集合元素  entrySet
        Set<Map.Entry<String, Integer>> entrys =   cards.entrySet();
        for(Map.Entry<String, Integer> en  :  entrys ){
            System.out.println("entry遍历方式:"+en.getKey()+"-----"+en.getValue());
        }
        // remove
        System.out.println("删除元素:"+cards.remove("红桃"));
        //清空元素
        cards.clear();
        System.out.println("元素的大小:"+cards.size());
    }

2、HashMap的原理以及源码分析

​ HashMap基于键值对存储,这里讲解的jdk8的源码

​ HashMap实现步骤:数据结构(数组+链表+红核数)

1、根据key生成对应是hash值(采用Hash函数生成) ,根据hash值找到该元素所在的数组结构中的位置,如果该位置中存在元素,说明产生了哈希冲突,此时JDK8采用元素尾插入法 (JDK7采用头插法) ,将元素放入链表的尾部,这样可能会形成一条长长的链表。

​ 2、当链表长度达到8时,此时会转成 红黑树结构(树形结构的检索效率较高) ,为了提高查询效率。

HashMap中的一些关键属性和方法

/**
 * 扩容的临界值,通过capacity * load factor可以计算出来。超过这个值HashMap将进行扩容
 * @serial
 */

int threshold; 
/**
 * 存储键值对的数组,一般是2的幂
 */
transient Node<K,V>[] table;

/**
 * 键值对的实际个数
 */
transient int size;


/**
 * 记录HashMap被修改结构的次数。
 * 修改包括改变键值对的个数或者修改内部结构,比如rehash
 * 这个域被用作HashMap的迭代器的fail-fast机制中(参考ConcurrentModificationException)
 */
transient int modCount;

/**
 * HashMap的节点类型。既是HashMap底层数组的组成元素,又是每个单向链表的组成元素
 */
static class Node<K,V> implements Map.Entry<K,V> {
        //key的哈希值
    final int hash;
    final K key;
    V value;
        //指向下个节点的引用
    Node<K,V> next;
    
}
        //增长因子  0.75 
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
   

几个常用方法分析:

1、get(Object)

 public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    

返回目标Node

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 整个数组长度不为空, 且第一个Node不为空   说明已找到对一个的hash位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 表示当前“桶”的第一个元素  的hash值相同,且 key也相同,说明value 是目标查找对象
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 说明一个“桶”中有多个元素,  继续找
            if ((e = first.next) != null) {
                // 多个元素中 需要先判断是否为 “树”结构,因为超过8个长度就转成了数
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 一定是单链表结构  ,依次从头找到尾,看有没有对应的 key 
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

2、put(K,V)

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
     // 获取元素的总长度 
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
       // 判断如果当前集合中没有对应的 “桶”,说明没有出现 “Hash碰撞”
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            
            Node<K,V> e; K k;
             //如果碰撞了,且桶中的第一个节点就匹配了
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果桶中的第一个节点没有匹配上,且桶内为红黑树结构,则调用红黑树对应的方法插入键值对
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                 //不是红黑树结构,那么就肯定是链式结构
                
                for (int binCount = 0; ; ++binCount) {
                    //如果到了链表的尾部 ,插入到后面 
                    if ((e = p.next) == null) {
                        // 创建一个新节点 创建最后节点 next中
                        p.next = newNode(hash, key, value, null);
                        // 如果长度大于 8 
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                             // 将tab 的桶的所有元素 转成  树结构 
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果还没有到达尾部 就找到元素了, 直接返回 
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
           // 如果找到该元素 ,需要替换它的  value  
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
     // 修改次数增加
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

put插入的流程

img

步骤一: 先判断容器是否为空,为空需要扩容,

步骤二: 根据key 生成hash值,根据hash值找到对应的数组的位置,如果数组位置为空,说明没有hash冲突,直接插入,并长度+1 。

步骤三:如果数组位置的内容不为空,说明产生hash冲突 ,继续通过key查找元素,如果第一个元素存在(说明还没有生产链表)直接返回value 并覆盖value的值。

步骤四: 如果key对应的第一个元素不存在,则此时可能出现链表或红黑树, 如果是红黑树采用树结构的插入法(省略分析过程)。 否则一定是链表结构

步骤五: 如果是链表,将该元素插入到末尾,之后验证整个链表的长度是否大于8 ,如果大于8,将链表转成红黑树结构。

步骤六:插入成功之后,判断整个容器的元素个数是否超出 扩容的临界值(threshold = capacity* 增长因子)

关于JDK7.0和JDK8.0的HashMap有什么区别

1、结构不同

​ JDK7采用数组+链表 JDK8采用数组+链表+红黑树

2、hash值的计算方式不同

​ JDK7 table在创建hashmap时分配空间

​ JDK8 通过key的hashcode计算,在put时分配空间

3、发生hash冲突插入方式不同

​ JDK7采用头插法,JDK8采用尾插法

4、resize操作方式不同

​ JDK7重写计算index 的值,JDK8通过判断相应的位是0还是1,要么依旧是原index,要么是oldCap + 原index

3、LinkedHashMap

由于HashMap存储的key是无序,如果需要存储有序的key可使用LinkedHashMap 它依然满足HashMap的所有特点 ,并在此基础上有序

  

4、TreeMap

TreeMap实现一个可排序的Map集合 ,默认对key升序排列,也可以降序排序,

​ 如果添加元素的key为自定义类,需要实现Comparable接口或Comparator接口。 TreeMap的底层实现是二叉树结构 (有关二叉树的特点 ) ,实现有序的key的分布

 // TreeMap 用于存储可排序的Key -Value集合  ,
    // 其中key必须实现了排序规则的对象 (包装类,String,自定类)
    public static void main(String[] args) {
        // 存储学号和分数   默认对key 进行升序
      //  TreeMap<Integer ,Integer> maps = new TreeMap<>();

        TreeMap<Integer,Integer> maps = new TreeMap<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2-o1;   // 降序
            }
        });

        maps.put(1003,88);
        maps.put(1002,90);
        maps.put(1004,80);
        maps.put(1001,85);
        //输出
        Set<Integer> keys =  maps.keySet();
        for(Integer k : keys){
            System.out.println(k+"----"+maps.get(k));
        }

        // 自定义规则
        // 注意 ,如果key不是包装类而是自定义,必须要求该类实现Comparable或Comparator接口
        TreeMap<Student ,Integer> stuMap = new TreeMap<>();
        stuMap.put(new Student(1001,"张飞"),90);
        stuMap.put(new Student(1003,"刘备"),87);
        stuMap.put(new Student(1002,"关羽"),96);
        // 这里降序排列
        for(Map.Entry<Student,Integer> en : stuMap.entrySet()){
            System.out.println(en.getKey().getSid() + "---"+ en.getValue());
        }
    }

class  Student implements  Comparable<Student>{
    private int sid;
    private String sname;

    public Student(int sid, String sname){
        this.sid = sid;
        this.sname = sname;
    }

    public int getSid() {
        return sid;
    }

    public void setSid(int sid) {
        this.sid = sid;
    }

    public String getSname() {
        return sname;
    }

    public void setSname(String sname) {
        this.sname = sname;
    }

    @Override
    public int compareTo(Student o) {
        return o.sid-this.sid; // this表示前一个对象,  o表示后一个对象
    }
}

5、HashTable

​ HashTable 实现 hash结构的key-value集合, 与HashMap很相似, HashTable 是线程安全(它的很多方法是同步操作),它不需要存储null的 key 和value

​ 扩展自 Dictionary类 和 实现Map接口

常用方法 :

​ put ()

​ get()

​ clear()

​ containsKey()

​ containsValue()

​ 它 有一个子类 是 Properties类,用于存储属性文件的 key- value

 public static void main(String[] args) {
         //创建HashTable   无序
        Hashtable<String,String> tables = new Hashtable<>();
        // 存储
        tables.put("王宝强","马蓉");
        tables.put("贾乃亮","李小璐");
        tables.put("文章","马伊琍");
        //获取   使用所有key遍历  返回枚举类型
        Enumeration<String> keys = tables.keys();
        while(keys.hasMoreElements()){
            String s = keys.nextElement();
            System.out.println(s + "---"+ tables.get(s));
        }

        // 有一个HashTable的子类  Properties
        Properties prop  = new Properties();
        prop.setProperty("username","张三");
        prop.setProperty("password","123456");
        //获取对应属性名的值
        System.out.println("根据属性名获取值:"+prop.getProperty("username"));
        System.out.println("根据属性名获取值:"+prop.getProperty("password"));

    }

集合常见面试题

1、Collection 和 Collections 的区别? Array 和Arrays的区别?

​ Collection是集合的顶级接口 ,Collections是一个集合工具类 ,它提供大量的操作集合方法,例如排序, 打乱顺序,添加元素等。

​ Array 表示一个数组对象 , Arrays是一个数组工具类 ,提供大量的数组操作方法。

2、List 和 Set 的区别? ArrayList 和 LinkedList的区别

​ 相同点:List、Set 都继承Collection接口

​ 不同点: List 存储不唯一,有序集合元素 , Set 存储唯一,无序的集合元素

​ ArrayList 实现数组结构集合,查询比较快,添加,删除效率低

​ LinkedList实现双向链表结构集合,添加,删除效率高, 查询效率低

3、HashSet 和 TreeSet的区别?

它们都存储唯一集合,都实现Set接口

HashSet无序,底层实现Hash结构的存储, TreeSet有序,可实现自定义排序,存储树形结构

4、HashMap 和 HashSet的区别?

它们都属于Hash结构的集合,存储效率较高 , HashSet是存储单个元素 ,HashMap存储键值对元素

HashMap实现Map接口,HashSet实现Set接口 ,

5、HashMap 和HashTable的区别

它们都来自Map 的实现类, HashTable 还继承一个父类 Dictionary

  • HashMap 的key和value 可以为空,HashTable 的key value 不能为空;
  • HashTable 的子类包含key value 的方法
  • HashMap线程不安全效率高 ,HashTable线程安全效率低

6、HashMap 和 TreeMap的区别?

​ 它们都实现Map接口 ,TreeMap 有序,HashMap无序, HashMap实现哈希结构 集合,TreeMap实现二叉树集合。

四十五、JDK8的特性

在JDK8中新增一些特殊功能,一般开发时方便使用, 其中最主要的功能如下

1、接口的默认函数

public interface MyInterface {

    public default void defaultMethods(){
        System.out.println("这是一个接口的默认方法。");
        // 静态方法可以在default方法中调用
        staticMethods();
    }

    public void service();

    public static void staticMethods(){
        System.out.println("这是接口的静态方法");
    }
}

 public static void main(String[] args) {
        // 创建匿名内部类的方式
        MyInterface my = new MyInterface() {
            @Override
            public void service() {
                System.out.println("service方法.....");
            }
        };
        my.defaultMethods();
        //通过接口名 调用静态方法
        MyInterface.staticMethods();

    }

2、Lambda表达式

​ JDK8中支持一种对方法调用的 简写方式 ,也是一种特殊写法

语法: ([形参名]) ->{ 方法的实现}

这个接口中有且只有一个方法,并对方法实现

原始代码

      ArrayList<String> list = new ArrayList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        list.add("ddd");
        // 降序
        Collections.sort(list, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o2.compareTo(o1); //降序
            }
        })

由于JDK能识别sort的参数二是Comparator , 并Comparator是函数式接口(一个接口中有且只有一个方法。),实现的一定是唯一的方法名, 所有这里方法名和返回值也省略, 它的简写方式

     // 使用lambda表达式   ([形参])->{方法体}  
        Collections.sort(list ,(String o1,String o2)->{
            return o2.compareTo(o1);
        });

由于形参的数据类与集合的元素类型一致,这里的形参类型也省略 、return也省略

// 最精简的Lambda
        Collections.sort(list ,(o1,o2)->o2.compareTo(o1));

3、函数式接口

函数式接口主要用于满足前面Lambda表达式的语法的使用。 在一个接口中 有且只有一个方法的接口称为 “函数式接口” ,

​ 如何将一个接口定义为函数式接口呢? 在接口上增加注解 “@FunctionalInterface”

package com.j2008.functionalFun;
@FunctionalInterface  // 该注解的意义在于 约束接口只能由一个方法
public interface ConverterInterface<T> {
    public String convertStr(T t);


}
    //传统写法
ConverterInterface<Integer> con = new ConverterInterface<Integer>() {
             @Override
             public String convertStr(Integer o) {
                 return  o.toString();
             }
         };

        String ss =  con.convertStr(100);
        System.out.println(ss);
	//使用 lambda表达式写法
        ConverterInterface<Date> cons = (o)->o.toLocaleString();
        String s =  cons.convertStr(new Date());
        System.out.println(s);

4、方法和构造器的引用

4.1 方法的引用

在以上接口函数中 这个convert方法的实现 对于java.lang包中是存在相同的方法的,所以 convert的实现可以直接引用已有静态方法 Integer.valueOf 或者 Integer.parseInt

​ 以上代码的实现可以改成这样 :

​ 语法: 类名 :: 静态方法名

@FunctionalInterface
public interface Convert<F,T> {
    //将F转成T
    public T convertType (F f);
}
 public static void main(String[] args) {
         // 原始使用lambda表达式 可以这样写
        // 把字符串转成  Integer
        Convert<String ,Integer> con = (f)->{
            return Integer.parseInt(f);
        };
        Integer n = con.convertType("888");

        // 在lambda基础上,如果实现的方法 已存在,则可直接调用  类名::方法名
        Convert<String ,Integer> con2 = Integer::valueOf;
        Convert<String ,Integer> con3= Integer::parseInt;

        //调用方法实现
        Integer n2 = con2.convertType("123");
        int n3 = con3.convertType("555");

 }

4.2 构造器的引用

当方法的实现 是构造器时,可直接引用构造器

语法: 类名::new

@FunctionalInterface
public interface StudentFactory<T> {
    // 参数是创建对象时 的 属性
    public T create(int id ,String name);
}
public class Student {
    private int sid;
    private String sname;
      public Student(int sid, String sname) {
        this.sid = sid;
        this.sname = sname;
    }
  }
 public static void main(String[] args) {
        //使用Lambda实现 函数接口的方法
        StudentFactory<Student> factory = (id,name)->{
            return new Student(id ,name);
        };
        Student stu1 =  factory.create(1001,"张三丰");
        System.out.println(stu1);

        // 以上写法可以直接换成 引用构造器方式
        StudentFactory<Student> factory1 = Student::new;
        //创建
        Student stu2 = factory1.create(1002,"张无忌");
        System.out.println(stu2);
    }

一、集合的流式处理

在JDK8以后,提供对集合的流式操作,对集合的元素可以向“流水”一样,依次方便,遍历,排序等,它是“不可逆的”(访问后面元素之后不能再次返回前面元素 ) , 根据流的处理方式不同,可以分为 串行流和并行流, 串行流表示同一时间只能有一个流式操作,而并行流可以有多个流式操作。

流返回的结果包括中间操作和 最终操作 

中间操作:它的返回值依然是 流对象 ,例如 排序、过滤、去重

最终操作: 返回值是特定的结果类型 ,例如 遍历,取最大值,最小值或返回新的集合

常用方法:

      stream() :将一个集合流式化

filter(): 按条件过滤,里面使用lambda表达式

sort():  排序集合元素

distinct: 过滤重复元素

   reduce() : 将集合的所有元素累加或拼接  

map(); 映射一个新的集合 对集合元素变更输出

collect():返回一个新集合 

  max() min():返回集合最大值或最小值

 get (): 获取集合计算的结果

 public static void main(String[] args) {
        List<Integer> list = new ArrayList();
        for(int i = 0 ;i<7;i++){
              list.add(i+1);
        }
         // 1、过滤 filter()   过滤掉偶数
        list.stream().filter( param ->param%2==1 )
                    .forEach(System.out::println); //遍历元素

        // 2、排序 sort()   降序
        list.stream().sorted((o1,o2)->o2-o1).forEach(System.out::println);

        //  3 map() 映射一个新的集合 , 如果是奇数 输出奇数 ,否则偶数
        list.stream().map(
                param -> param%2==1?"这个元素是奇数":"这是偶数"
                ).forEach(System.out::println);

        list.add(1);
        list.add(1);
        System.out.println("去重元素");
        // 4 distinct()  去除重复元素
        list.stream().distinct().forEach(System.out::println);

        // 5 reduce() 将集合的所有元素 累加(或拼接)

        int sum =  list.stream().reduce((o1,o2)->o1+o2).get();
        System.out.println("总和:"+sum);

        // 6 collect 返回一个新的集合
        List<Integer> list2= list.stream().filter(param->param%2==1).collect(Collectors.toList());
        System.out.println("遍历新集合");
        list2.stream().forEach(System.out::println);

        // 7、最大和最小
        int max = list2.stream().max((o1,o2)->o1-o2 ).get();
        System.out.println("最大值:"+max);
        int min = list2.stream().min((o1,o2)->o1-o2 ).get();
        System.out.println("最小值:"+min);
    }

集合的流式处理

在JDK8以后,提供对集合的流式操作,对集合的元素可以向“流水”一样,依次方便,遍历,排序等,它是“不可逆的”(访问后面元素之后不能再次返回前面元素 ) , 根据流的处理方式不同,可以分为 串行流和并行流, 串行流表示同一时间只能有一个流式操作,而并行流可以有多个流式操作。

流返回的结果包括中间操作和 最终操作 

中间操作:它的返回值依然是 流对象 ,例如 排序、过滤、去重

最终操作: 返回值是特定的结果类型 ,例如 遍历,取最大值,最小值或返回新的集合

常用方法:

      stream() :将一个集合流式化

filter(): 按条件过滤,里面使用lambda表达式

sort():  排序集合元素

distinct: 过滤重复元素

   reduce() : 将集合的所有元素累加或拼接  

map(); 映射一个新的集合 对集合元素变更输出

collect():返回一个新集合 

  max() min():返回集合最大值或最小值

 get (): 获取集合计算的结果

 public static void main(String[] args) {
        List<Integer> list = new ArrayList();
        for(int i = 0 ;i<7;i++){
              list.add(i+1);
        }
         // 1、过滤 filter()   过滤掉偶数
        list.stream().filter( param ->param%2==1 )
                    .forEach(System.out::println); //遍历元素

        // 2、排序 sort()   降序
        list.stream().sorted((o1,o2)->o2-o1).forEach(System.out::println);

        //  3 map() 映射一个新的集合 , 如果是奇数 输出奇数 ,否则偶数
        list.stream().map(
                param -> param%2==1?"这个元素是奇数":"这是偶数"
                ).forEach(System.out::println);

        list.add(1);
        list.add(1);
        System.out.println("去重元素");
        // 4 distinct()  去除重复元素
        list.stream().distinct().forEach(System.out::println);

        // 5 reduce() 将集合的所有元素 累加(或拼接)

        int sum =  list.stream().reduce((o1,o2)->o1+o2).get();
        System.out.println("总和:"+sum);

        // 6 collect 返回一个新的集合
        List<Integer> list2= list.stream().filter(param->param%2==1).collect(Collectors.toList());
        System.out.println("遍历新集合");
        list2.stream().forEach(System.out::println);

        // 7、最大和最小
        int max = list2.stream().max((o1,o2)->o1-o2 ).get();
        System.out.println("最大值:"+max);
        int min = list2.stream().min((o1,o2)->o1-o2 ).get();
        System.out.println("最小值:"+min);
    }

四十六、Java的I/O

1、什么是I/O?

在生活中,你需要将U盘的文件 ,拷贝到电脑(或者将电脑的文件拷贝到其他设备), 文件是通过数据流的方式依次到达另一个设备中, 文件的拷贝就是一个输入(Input)和输出(Output)的过程

Java中提供对应的API支持对文件的输入和输出 , java.io.*

2、什么是流?

生活中 也存在流的概念,例如 管道中的流水,从管道的入口到达管道出口,一滴水可以从入口流到出口,可以将“水”比作 “字节数据或字符数据”,数据也可以从一端流到另一端。

输入(Input): Java中,以“应用程序(内存)”为中心,将磁盘文件(设备端)到达内存中的过程 称为 输入

输出(Output): 以“应用程序(内存)”为中心,将数据从内存到达磁盘文件(设备端)的过程称为 输出

在这里插入图片描述

根据文件操作方式不同可以将IO分为三类

1、按照读写方向不同: 输入流(InputStream)和输出流(OutputStream)

2、按照数据类型不同: 字节流(Byte)和字符流(Char)

3、按照读写效率不同: 单一流和包装流(buffered等缓冲流)

关于流的分类Java提供 4个顶级抽象类 ,分布构建它们的子类

输入流输出流
字节流InputStream(字节输入流)OutputStream(字节输出流)
字符流Reader(字符输入流)Writer(字符的输出流)

常见的流

1、文件字节输入流和文件字节输出流 : FileInputStream 和 FileOutputStream

2、文件字符输入流和文件字符输出流: FileReader 和 FileWriter

3、缓存字节输入流和 缓存字节输出流 BufferedInputStream 和 BufferedOutputStream

4、缓存字符输入流和缓冲字符输出流 BufferedReader 和BuffereWriter

5、数据输入流和数据输出流: DataInputStream 和 DataOutputStream

6、字节数组输入流 和 字节数组输出流 : ByteArrayInputStream 和 ByteArrayOutputStream

7、字符数组输入流 和 字符数组输出流: CharArrayReader 和 CharArrayWriter

8、转换流: (字节流转成字符流) InputStreamReader 和 OutputStreamWriter

9、对象流(序列化流): ObjectInputStream 和 ObjectOutputStream

10、随机访问流(这个流既可以读,也可以写):RandomAccessFIle

在这里插入图片描述

字节流

定义: 文件的输入输出以一个“字节”为单位,进行流处理

FileInputStream 和 FileOutputStream

读入: 将文件中的数据读到内存中

常用方法:
	int read() : 一个字节的读取 ,返回字节 的asci码 ,对于汉字会分3次读取 

            int read(byte) : 按一个数组长度读取, 返回实际读取的字节长度, 数据存放在数组中 

           int read(byte , offset , len) : 读取流中指定长度的数据,并存放在指定位置,

            available() :返回流中剩余的字节长度,如果已读完,则返回0 

           skip(long n ): 丢弃指定的字节长度,从下一个开始读取
File file = new File("d:/aaa.txt");
        FileInputStream fis = new FileInputStream(file);
       //
		byte [] b= new byte[10];
        StringBuffer sb = new StringBuffer();
            //每次读取的长度,  b: 存放数据的数组
          int len = 0;
          while((len = fis.read(b)) !=-1){
              sb.append( new String(b,0,len));
          }

        System.out.println(sb);
        fis.close();
 public static void read2() throws IOException {
        // InputStream是抽象类
        InputStream is = new FileInputStream("d:/aaa.txt");
        //丢弃前两个字节
        is.skip(2);
        System.out.println((char)is.read());
        System.out.println("还剩下多少个字节:"+ is.available());
        // 将后面的字节继续使用字节数组读
        byte [] b = new byte[10];
        int len = is.read(b,1,4);
        // 显示数组中读取的所有数据
        System.out.println(Arrays.toString(b));
        //将数组的内容转成字符串  对于空内容不会转换
        System.out.println(new String(b));

        is.close();


    }

文件写出: 将内存的数据写出到磁盘中

构造方法:

new FileOutputStream(File/String ) : 构造文件对象的写出流, 默认覆盖写出

new FileOutputStream(File/String , append): 构造文件对象的写出流,

append:表示在原有文件上追加数据, false : 覆盖

常用方法:

 void write(int) : 写出一个字节 

    void writer(byte []) :写出一个字节数组,这里需要指定数组的编码格式 “UTF-8”

   void writer(byte[]  , offerset,len) : 写出一个字节数组,指定数组的长度和下标。 从数组的下标开始写出,len表示写出长度

  flush() :清空缓存,对于使用缓冲流时,将缓冲强制清空。
  //将内存的数据写出到文件   如果文件不存在,会自动创建, 默认覆盖写入  true:追加
        FileOutputStream fos = new FileOutputStream("d://aaa.txt" ,true);
        String str="今天天气还不错";
        fos.write(99);
        //写出一个字符串    字符串可以转成字节数组 或字符数组
        fos.write(str.getBytes("UTF-8"));
        // 写出指定长度
        fos.write(str.getBytes("UTF-8"),0,3); // 写出这个数组的前2个字节
        // 清空缓存
        fos.flush();
        // 关闭流
        fos.close();
        System.out.println("写出成功");```
文件复制: 

将文件(图片,文本,视频)从一个目录复制到另一个目录, 其中数据长度不变,通过文件读写的方式完成复制

复制过程:从源文件读取数据,然后将数据再出到目标文件中。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20201106141928428.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMxNjE0MjEx,size_16,color_FFFFFF,t_70#pic_center)

```java
/**
     * 单个字节复制
     * @param srcFile 源文件
     * @param disFile 目标文件
     */
    public static void copyFile(File srcFile, File disFile){
        FileInputStream fis=null;
        FileOutputStream fos =null;
        try {
            // 源文件输入流
             fis = new FileInputStream(srcFile);
            // 目标文件输出流
             fos = new FileOutputStream(disFile);
            int n=0;
            while( (n =fis.read()) !=-1){
                 //将读到的n写出到 目标文件中
                 fos.write(n);
             }
            System.out.println("复制成功。。");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally{
            //无论是否发生异常 都会关闭流
            try {
                fos.close();
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

    }

    /**
     * 一个字节数组的赋值
     * @param src  源地址
     * @param disc 目标地址
     */
    public static void copyFile(String src,String disc){
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            //创建 字节输入流
            fis=new FileInputStream(src);
            fos = new FileOutputStream(disc);
            int len=0;
            byte [] b = new byte[1024];
            while( (len= fis.read(b)) !=-1){
                // 写出 实际读取的长度 ,为了避免在最后一次写出时出现多余字节
                fos.write(b,0,len);
            }

            System.out.println("复制成功");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally{
            try {
                fos.close();
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

字符流

字符流用于读写存储字符的文件, 以一个字符为单位,一依次读取字符 文件, 常用类以 Reader或Writer为父类, 对文件的操作使用 java.io.FileReader 和java.io.FileWriter       

读文件 :FileReader

常用方法:

new FileReader(path): 通过文件路径构建字符输入流

new FileReader(File):通过文件对象构建字符输入流
  • int read() :读取一个字符 ,返回字符的int类型
    int read(char ):读取字符数组长度的数据 ,返回实际读取字符长度,数据存放在字符数组中
    int read(char offerset len):读取指定字符长度的数组,返回 实际读取字符的长度,数据存放在字符数组中
    mark(int) :标记流中当前位置 (读取到哪里了)
    markSupported():判断此流是否支持mark操作
  •     reset(): 重置流数据,(又可从头开始读取)
    
    skip(long) :丢弃指定长度字符

读字符文件

  // 1、创建字符输入流
        try {
            FileReader reader = new FileReader("d:/myfile.txt");
            // 丢弃字符
            reader.skip(1);
            //读一个字符
            System.out.println((char)reader.read());
            System.out.println((char)reader.read());
            //读一个字符数组长度
            char [] c = new char[10];
            System.out.println("实际长度:"+reader.read(c));
            System.out.println(new String(c));

            //继续读
            int len =  reader.read(c,0,5);
            System.out.println("字符数组:"+ Arrays.toString(c));
            System.out.println("读指定长度字符个数:"+new String(c,0,len));

            // 将字符流重置
            // reader.reset();
            // System.out.println("重置后继续读:"+ reader.read());

            //System.out.println("是否支持标记字符:"+reader.markSupported());
            //关闭流,后面就不能使用该对象
            reader.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

写文件: 将内存数据写出到文件中,在写出过程中可以 覆盖写出也可以追加写出,FileWriter类创建对象过程

   new FileWriter(String ):指定写出文件地址

new FileWriter(String ,append) : 指定写出文件地址,设置是否追加写出,true表示追加,false表示覆盖

new FileWriter(File)指定写出文件对象

new FileWriter(File ,append);指向写出文件对象,设置是否可追加
常用方法:

writer(int) :写出一个字符

writer(String):写出一个字符串

writer(char [] ):写出一个字符数组

writer(char [] , offerset , len):写出一个指定长度的字符数组

flush() :刷新缓冲, 

close():关闭缓冲 

append(c) :将指定字符添加到此流中
  // 1、创建文件写出流 FileWriter
        try {
            // 文件不存在,可自动创建,但是不会创建目录
            File file = new File("d://myabc/aaa.txt");
            //判断文件目录不存在, 先创建目录
            if(!file.getParentFile().exists()){
                //创建该目录
                file.getParentFile().mkdirs();
            }

            FileWriter writer = new FileWriter("d://myabc/aaa.txt");
            // 写一个字符的asci
            writer.write(98);
            //写字符串
            writer.write("hello");
            //写指定长度的字符串
            writer.write("abcdef",0,3); //写abc
            char [] c = {'L','O','L'};
            //写字符数组
            writer.write(c);
            System.out.println("写出成功");
            writer.flush();
            writer.close();

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

关闭和刷新:

对于带有缓冲 功能的写出流,需要先刷新缓冲区,才能将数据写出,如果不刷新则最后不能正常写出。写出流如果刷新后还可以继续写,而关闭了则不能继续写。

面试题 flush 和close的区别?

flush: 刷新缓冲 ,流可以继续使用

close: 先刷新缓冲器,然后再释放系统资源, 关闭后不能继续使用

   try {
            FileWriter writer = new FileWriter("1.txt");
            writer.write("刷");
            writer.flush();
            writer.write("新");
            writer.flush();

            writer.write("关");
            writer.close();
            writer.write("闭"); // 这里抛出异常 , Stream closed
            writer.close();


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

关于换行符

回车符 \r 和换行符 \n :

回车符:回到一行的开头(return)。

换行符:下一行(newline)。

系统中的换行:

Windows系统里,每行结尾是 回车+换行 ,即 \r\n ;

Unix系统里,每行结尾只有 换行 ,即 \n ;

Mac系统里,每行结尾是 回车 ,即 \r 。从 Mac OS X开始与Linux统一。

包装流

定义: 在原始字节流或字符流的基础性,为了提高读写效率进行再次处理的流, 称为包装流/处理流

1、缓存字节流 BufferedInputStream 、BufferedOutputStream

由于原始流在文件读写时 效率比较低(操作文件本身占用资源较多),可以通过创建缓冲区的方式提高读写效率, 将读取/写出的数据线放入缓冲区,到达一定数量后再次冲缓冲区读取/写出

 mark(readLimit) 与   reset()用法

其中reset不能单独使用,必须mark(readLimit) ,readLimit表示标记后最多读取的上限,但是这里标记后读取的内容与BufferedInputStream的缓冲大小有关,比由上限决定,也就是说读取的内容超出上限可以继续重置到mark的位置。

public static void main(String[] args) throws IOException {
        //创建缓冲流
        InputStream is = new FileInputStream("d:/myfile.txt");
        BufferedInputStream bis = new BufferedInputStream(is);
        //是否支持mark  或 reset
        System.out.println(bis.markSupported());
        System.out.println((char)bis.read());//97
        //重置
        bis.mark(3);  // pos标记往后退三个  最多可以读取字节上限

        System.out.println("再次读取:"+(char)bis.read());
        System.out.println("再次读取:"+(char)bis.read());
        System.out.println("再次读取:"+(char)bis.read());
        System.out.println("再次读取:"+(char)bis.read());

       bis.reset(); // 这里 重置后 退回到3个以前的位置

        // 重置后输出
        int n =0;
        while( (n = bis.read()) !=-1){
            System.out.println("重置后;"+(char)n);
        }
        //关闭流
        bis.close();
        is.close();

    }
2、缓存字符流 (BufferedReader 、BufferedWriter)
 public static void main(String[] args) throws IOException {
        // 缓冲字符流 可以一行一行读取   、写出
        BufferedReader br = new BufferedReader(new FileReader("d:/小众网站.txt"));
        //读一行
//        System.out.println(br.readLine());
//        System.out.println(br.readLine());
//        System.out.println(br.readLine());
        String s = null;  //读的数据为空 则不需要读
        while( (s = br.readLine()) !=null){
            System.out.println(s);
        }

        br.close();

        //缓冲写出流
        FileOutputStream   pw =  new FileOutputStream("d:/abcd.txt");
        //由于字节流不能直接放入 字符缓冲区,需要将它转成字符流  使用转换流并可以指定编码格式
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(pw));
        bw.newLine();// 开启新一行(换行)
        bw.write("这是测试转换流的方式");

        bw.close();
    }
3、打印流(输出流) PrintWriter 、PrintStream
 public static void main(String[] args) throws FileNotFoundException {
        // 打印流 ,提供一些打印输出方法

        PrintWriter pw = new PrintWriter("d:/abcd.txt");
        pw.print(100);
        pw.println('a');//换行打印
        pw.print("hello");


        pw.close();

        //System.out   字节打印流 PrintStream
4、数据字节流DataInputStream、DataOutputStream
它们用于读入写出Java基本数据类型的数据到文件或其他设备端,它们也属于包装流

DataOutputStream 常用方法

  •    writerByte(byte):写一个字节到设备或文件
    
  •    writerChar(char):写一个字符到设备或文件
    
  •    writerInt(int):写一个4个字节的int到设备或文件
    
    writer(boolean):写一个boolean类型到设备或文件
    writerDouble(double):写一个double类型到设备或文件
    writerFloat(float):写一个float类型到设备或文件
    writerLong(long):写一个long类型到设备或文件
    writerShort(short):写一个short类型到设备或文件
    writerUTF(String):写一个字符串类型到设备或文件
DataInputStream: 读指定文件的数据,可以读数据类型
  •   int  readInt() :读一个int类型
    
  •   short readShort():读一个short类型
    
    readByte():读一个字节类型
    read():读一个字节类型
    readDouble(): 读一个double类型
    readFloat():读一个float类型
    readChar():读一个字符类型
    readBoolean():读一个boolean类型
    readLong() :读一个long类型
public static void main(String[] args) throws IOException {
        //创建数据写出流
        DataOutputStream dos = new DataOutputStream(
                new FileOutputStream("d:/data.txt"));
        //写一个int类型 依次写出4个字节
        dos.writeInt(100);
        dos.writeBoolean(true);
        //关闭
        dos.close();
     
     //读取文件 创建数据读入流  ,需要按写的顺序读进来
        DataInputStream dis = new DataInputStream(
                new FileInputStream("d:/data.txt"));
        //读一个int类型 (依次读4个字节)
        int num = dis.readInt();
        System.out.println("读取的数据:"+ num);
        System.out.println("读的数据:"+dis.readBoolean());
        dis.close();


    }
5、转换流
 转换流是将字节流转成字符流的桥梁, 也可以在转换时指定编码格式。 InputStreamReader 和 OutputStreamWriter   
   public static void main(String[] args) throws IOException {
         // 字节流转成字符流
         InputStream is = new FileInputStream("d://小众网站.txt");
         InputStreamReader isr = new InputStreamReader(is);
         //缓冲流 读取数据
         BufferedReader br = new BufferedReader(isr);
         //读一行
        String str =null;
        while( (str= br.readLine()) !=null){
            System.out.println(str);
        }
        //关闭流
        br.close();
        isr.close();
        is.close();

    }
 public static void main(String[] args) throws IOException {
        // 创建 字节转成字符的 写出流  FileOutputStream os  =
        FileOutputStream fos = new FileOutputStream("d://data.txt");
        //指定编码  GBK 格式一个汉字占2个字节   UTF-8 格式一个汉字占3个字节
        OutputStreamWriter osw = new OutputStreamWriter(fos,"UTF-8");
        //缓冲形式的
        BufferedWriter bw = new BufferedWriter(osw);

        bw.write("你好");
        bw.newLine();
        bw.write("我不好");

        bw.close();


    }
6、随机字节流

RandomAccessFile 是随机字节流,它是一个可读可写的流 ,在文件操作时指定该对象的模式(model)后,可以读数据或写数据

实现 DataInputStream和DataOutputStream类

构造器:

RandomAccessFile rm = new RandomAccessFile(File ,mode);

  RandomAccessFile  rm = new RandomAccessFile(String ,mode);

mode表示对象的模式

r: 表示该对象只能读 不能写

rw/rws/rwd :表示该 对象是可读可写的;

public static void main(String[] args) throws IOException {
        //创建可读 的流
        RandomAccessFile reader = new RandomAccessFile("d://data.txt","r");
        //创建可读可写的 的流
        RandomAccessFile writer = new RandomAccessFile("d://data-1.txt","rw");
        // 读和写的过程和之前一样
        byte [] b= new byte[10];
        int len=0;
        while( (len = reader.read(b)) !=-1){
            writer.write(b , 0 , len);
        }
        System.out.println("复制成功");
        //关闭流
        writer.close();
        reader.close();

    }

skipByte 和 seek的区别

      // 跳字节读取
        RandomAccessFile raf = new RandomAccessFile("d:/data.txt","rw");
        // 跳过2个字节
        raf.skipBytes(2);
        System.out.println((char)raf.readByte()); //3
        System.out.println("当前偏移量:"+raf.getFilePointer());//3
        // 又丢弃1个字节   从当前位置 往后偏移1位
        raf.skipBytes(1);
        System.out.println("修改后的偏移量"+raf.getFilePointer());//4
        System.out.println("偏移后的读取数据:"+(char)raf.readByte()); //5
        raf.close();


        // seek用法
        RandomAccessFile raf2 = new RandomAccessFile("d:/data.txt","rw");
        //  设置当前读取的位置  ,从0开始计算  ,指定n ,就从n的下一个字节 读取
        raf2.seek(2);
        System.out.println("seek后的数据:"+(char)raf2.readByte());//3
        raf2.seek(1); // 又从0开始 设置偏移量为1  
 		System.out.println("修改后的偏移量"+raf.getFilePointer());//1
        System.out.println("seek后的数据:"+(char)raf2.readByte())//2
        raf2.close();
7、对象序列化流

对象流也称为序列化流,用于存储对象和读取对象的字节流,也是属于包装流

序列化和反序列化

  将内存中的对象(Object,集合类等)保存到磁盘、网络介质、其他设置的过程,并在合适的时间能获取磁盘文件/网络的数据 ,这个过程就是对象的序列化和反序列化。 

在这里插入图片描述

为什么需要序列化和反序列化呢?
在之前文件中存储的文本信息,这样不便于对数据的分类和操作,如果可以做到直接对对象的读和写这样可大大提高编程效率,并最大程度保证对象的完整性。

Java-IO中实现对象序列化的两种方式:

  1. 实现Serializable接口
  2. 实现Externalizable接口
Serializable接口

对象需要实现该接口,但是它没有任何需要实现的方法,只有一个用于标记该类可序列化的唯一标识。 任何类需要序列化都必须标记该变量

 public class User implements Serializable {
       // 对于能区分其他类的唯一表示
       private static final long serialVersionUID = 1L;
   
        private int uid;
        private String name;
        private String password;
        //  有一部分属性不能序列化
   
       public int getUid() {
           return uid;
       }
   
       public void setUid(int uid) {
           this.uid = uid;
       }
   
       public String getName() {
           return name;
       }
   
       public void setName(String name) {
           this.name = name;
       }
   
       public String getPassword() {
           return password;
       }
   
       public void setPassword(String password) {
           this.password = password;
       }
   
       @Override
       public String toString() {
           return "User{" +
                   "uid=" + uid +
                   ", name='" + name + '\'' +
                   ", password='" + password + '\'' +
                   '}';
       }
   }
      //创建序列化的对象流  从内存到文件
           ObjectOutputStream oos = new ObjectOutputStream(
                   new FileOutputStream("d:/user.txt"));
           User user= new User();
           user.setUid(1001);
           user.setName("admin");
           user.setPassword("123456");
           //序列化对象
           oos.writeObject(user);
           //关闭流
           oos.close();
   
           // 反序列化:  将文件中的数据 再读入到内存中 ,需要一个读的流 ObjectInputStream
           ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d://user.txt"));
           //  反序列化尽量只读一次 (也可以读多次, 如何写出就如何读入)
           Object obj = ois.readObject();
           if(obj instanceof  User){
               User u = (User)obj;
               System.out.println("反序列化的结果:"+u);
           }
           //关闭流
           ois.close();
问题: 能否自定义序列化的属性  ,这里可以采用方式二,实现Externalizable,并重写两个方法 接口继承而来,在其基础上新增了两个未实现方法:readExternal(ObjectInputStream)和 writeExternal(ObjectOutputStreawm)  ,自定义需要序列化的属性
public interface Externalizable extends java.io.Serializable
Externalizable接口
public class Student implements Externalizable {
    private  int id;
    private String name;
    private String sex;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    // 自定义可序列化的属性
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(this.id);
        out.writeUTF(this.name);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.id = in.readInt();
        this.name = in.readUTF();
    }

    public Student(int id, String name, String sex) {
        this.id = id;
        this.name = name;
        this.sex = sex;
    }

    public Student( ) {

    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                '}';
    }
}

 public static void main(String[] args) throws IOException, ClassNotFoundException {
         // 创建序列化类
        ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("d:/stu.txt"));
        //创建学生
        List<Student> list = new ArrayList<>();
        list.add(new Student(1001,"张飞","男"));
        list.add(new Student(1002,"刘备","男"));
        list.add(new Student(1003,"小乔","女"));
        // 将集合序列化
        oos.writeObject(list);
        //关闭
        oos.close();

        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("d:/stu.txt"));
        //读
        Object obj = ois.readObject();
        if(obj instanceof  List){
            List<Student> list2 = (List<Student>)obj;
            for(Student s : list2){
                System.out.println(s);
            }
        }
        //关闭流
        ois.close();



    }

问题: 哪些属性不能实现序列化

   1、类中的static修饰的属性不能序列化

2、类中属性被transient修饰的不能序列化 例如 transient private Integer age = null;

  3、实现Externalizable接口的类的属性不能全部序列化,必须手动写可序列化的属性。
8、文件的压缩流和解压流
1、为什么需要使用压缩文件

文件压缩使用场景: 在文件上传或下载中需要操作多个文件时,如果一个一个复制需要花较长时间,而且比较繁琐,javaAPI提供一种压缩/解压文件方式,可以将多个文件打包成一个文件(.zip)

包: java.util.zip

常用类: ZipEntry: 表示压缩文件中的每一个实体文件

	ZipFile: 表示压缩文件对象 

	ZipOutputStream:  表示压缩文件输出流,用于将普通文件写出到zip文件中 

	ZipInputStream: 表示解压文件的输入流,用于读zip文件中的每一个实体ZipEntry 

在这里插入图片描述
这里的abcd.txt就是一个ZipEntry

2、压缩文件步骤

在这里插入图片描述
a、创建需要压缩文件的输入流(InputStream )

  b、创建压缩包所在的路径,并指定压缩文件名,同时创建ZipOutputStream输出流

  c、将文件对象 添加到 ZipOutputStream中的实体中(也可以指定压缩后的实体名称)

   d、文件复制 

   e、关闭流
 public static void main(String[] args) throws IOException {
         // 1、创建文件对象
        File file = new File("d:/小众网站.txt");
        // 2、创建文件的输入流
        FileInputStream fis  = new FileInputStream(file);
        // 3、创建文件压缩流(输出流)
        ZipOutputStream zos = new ZipOutputStream(
                new FileOutputStream("d:/myfile.zip"));

        // 给压缩包中添加文件,并可自定义文件名
        zos.putNextEntry(new ZipEntry("小众网站.txt "));
        // 给压缩包设置注释
        zos.setComment("这是压缩包的注释。。。。");
        // 文件复制
        int len = 0;
        byte [] b = new byte[1024];
        while( (len = fis.read(b)) !=-1){
            zos.write(b,0,len);
        }
        System.out.println("文件压缩成功");

        zos.close();
        fis.close();

    }

压缩多个文件

 /**
     * 压缩一个文件夹  myfile
     * @param args
     */
    public static void main(String[] args) throws IOException {

        //构建压缩包的输出流
        ZipOutputStream zos = new ZipOutputStream(
                        new FileOutputStream("d:/myfile.zip"));
        File file=new File("d:/myfile");
        File [] files =  file.listFiles();

        for(File f : files){
             //构造每一个文件的输入流
            FileInputStream fis  = new FileInputStream(f);
            putZipFile(fis, zos ,f.getName());
            System.out.println(f.getName()+"文件压缩成功" );
        }

        //关闭压缩流
        zos.flush();
        zos.close();


    }

    /**
     * 将文件放入压缩流中
     * @param fis
     * @param zos
     * @param entryName
     * @throws IOException
     */
    public static void putZipFile(InputStream fis ,
                                  ZipOutputStream zos,
                                  String entryName) throws IOException {
        // 给压缩包中添加文件,并可自定义文件名
        zos.putNextEntry(new ZipEntry(entryName));
        // 给压缩包设置注释
        zos.setComment("这是压缩包的注释。。。。");
        // 文件复制
        int len = 0;
        byte [] b = new byte[1024];
        while( (len = fis.read(b)) !=-1){
            zos.write(b,0,len);
        }
        System.out.println("文件压缩成功");


        fis.close();
    }

3、解压文件步骤

解压文件是将一个.zip文件的内容,复制到文件下,需要使用ZipInputStream

解压文件的关键点: 获取解压文件的每一个条目ZipEntry的输入流 ,将输入流写出到指定位置。

如何获取输入流: ZipFile对象 表示一个zip文件
在这里插入图片描述
步骤:

   a、根据文件路径 创建ZipInputStream   

   b、根据文件路径创建ZipFile对象 

   c、循环遍历每一天条目, 得到它的ZipEntry 

   d、获取ZipEntry的输入流

   e、将文件复制到指定位置  
 public static void main(String[] args) throws IOException {
          //1、创建ZipInputStream
        ZipInputStream zis = new ZipInputStream(
                new FileInputStream("d:/myfile.zip"));
        // 2、创建ZipFile对象
        ZipFile zipFile = new ZipFile("d:/myfile.zip");
         // 3、获取zip中的实体
        ZipEntry en = null;
        while(  (en= zis.getNextEntry())!=null){
            System.out.println(en.getName()+"--"+en.isDirectory());
            //4、获取每一个en的输入流 (关键)
            InputStream is =   zipFile.getInputStream(en);
            copyFile(is ,"d:/my",en.getName());
        }
    }

    /**
     * 通过输入流 复制文件到指定的目录下
     * @param is  输入流
     * @param path  存放文件的路径
     * @param fileName  文件名
     */
    public static void copyFile(InputStream is , String path , String fileName) throws IOException {
          File file = new File(path);
          //判断目录是否存在, 不存在就 创建
          if(!file.exists()){
              file.mkdirs();
          }

        FileOutputStream fos = new FileOutputStream(path+File.separator+fileName);
        int len = 0 ;
          byte [] b = new byte[1024];
          while( (len = is.read(b)) !=-1){
                fos.write(b,0,len);
          }
          System.out.println("解压成功");
          fos.close();
          is.close();
    }

四十七、Java的多线程

1、线程的基本概念

1.1 定义

引入线程: 打开计算中的任务管理器,有很多条目,每一条目对应一个应用程序,这个应用程序我们称之为 “进程” ,每一个进程都占用CPU资源和内存, 在这一个进程中 包含多个任务,他们可以“同时”运行, 这里的每一个任务称为”线程“

如果将Java的 应用程序比作一个进程,那么它包含的多个执行流程就是一个 线程。

生活中的多线程: 你现在正在玩游戏 ,你可以一边聊天(互喷),你也可以操控游戏,还可以问候队友。玩游戏就是一个进程,你的不同的操作对于游戏本身就是一个单线程,如果你可以同时操作,就是游戏可支持 多线程。

进程:进程是计算机中独立的应用程序,进程是动态,可运行的

线程:在进程中运行的单一任务,是进程的子程序

程序: 程序是数据描述和操作代码的集合,它是完成某个功能的代码,它是静态的
在这里插入图片描述
多线程: 一个进程中的多个子任务, 在多线程中会出现资源抢占问题, 在单核CPU下同一时间点某个进程下只能有一个线程运行。线程与线程之间会互抢资源

CPU资源分配
在这里插入图片描述
电脑可以运行多个应用程序(多进程),在同一时间片,CPU只能执行某一个任务,由于时间片切换非常快,你根本不能察觉会出现“等待”的情况,如果电脑出现 “卡死” 你可以任务资源没有获取并正在等待中。

单线程运行流程:程序只有一条运行线路,从开始到结束保持一致
在这里插入图片描述
如何创建多线程的程序呢?

方式一: 继承Thread类

a、 定义一个类继承Thread类,重写run方法

b、创建该类的对象, 并调用start方法

public class MyThread extends  Thread {
    @Override
    public void run() {
         for(int  i=0;i<100;i++){
             //获取当前线程名
             System.out.println(this.getName()+"----"+i);
         }
    }
}

   public static void main(String[] args) {
         // 创建线程对象
         MyThread my = new MyThread();
         //开启线程
         my.start();

          for(int i = 0 ;i < 100 ;i ++){
            System.out.println("主线程的 i-----"+i);
        }
        // 结论: 对于多线程之间它们的执行过程会存在资源抢占,谁先获得cpu资源,谁就执行
    }

方式二:实现Runnable接口

a、创建一个类实现一个接口

public class MyThread2 implements Runnable {
    @Override
    public void run() {
        for(int i = 0;i<100;i++){
            //获取当前 线程的线程名
            System.out.println(Thread.currentThread().getName()+"----"+i);
        }
    }
}

b、借助Thread类开启线程

    public static void main(String[] args) {

         // 由于 MyThread2 与线程无关联,需要借助线程类完成启动
        // 创建线程需要执行的任务类
         MyThread2 my = new MyThread2();

        Thread th = new Thread(my,"线程A");
        th.start();

        //再启动一个
         Thread th2 = new Thread(my,"线程B");
         th2.start();

问题:以上两种创建线程的区别?

1、继承方式适用于没有直接父类 ,相对简单 ,是单一继承, 而接口的方式目标类既可以继承类还可以实现其他接口

2、Runnable实现方式适用于 资源共享,线程同步情况。

3、Runnable实现方式并不是线程类,而是实现线程的目标类(Target)

补充: 创建线程并非只有以上两种方式,还可以通过匿名内部的方式创建线程和 线程池的方式。

2、线程的生命周期

生命周期定义

线程从创建到销毁的整个过程,称为线程生命周期, 好比人的生命周期就是从出生到去世的整个过程中间会经历的过程包括 出生,长大,变老,离开 都是一个人要经历的。

生命周期的阶段

1、新生状态 : 程序创建该线程(实例化对象)

2、就绪状态(可运行状态) : 当线程对象调用start()方法后 ,可以抢占cpu资源,但不会立马运行run方法

3、运行状态: 当抢占到资源后,立马运行run方法

4、阻塞状态: 在运行过程中,线程遇到阻塞事件(线程休眠,wait ,IO操作,join操作等),变为阻塞状态

5、死亡状态: 线程运行完毕,或异常中断 ,此时CPU资源被释放
在这里插入图片描述

3、线程的常用方法和API

java.lang.Thread 类中提供了大量的相关的方法:

new Thread();

new Thread(name);

new Thread(Runnable,name);

new Thread(Runnable)

常用方法:

getId() :获取线程的唯一标识

getName() :获取线程名

getPriority():获取线程的优先级: 优先级从1-10 , min-priority:1 max-priority:10 norm- priority:5 注意说明优先级高的获取到cpu资源的概率越大,并不是一定会优先执行完成。

currentThread():获取当前线程的对象引用

getState():获取线程的状态 (这是返回线程状态的枚举, NEW:未启动,RUNABLE:可运行 BLOCK:阻塞状态, WAITING:等待状态TIMED-WAITIN: 等待另一个线程执行完)

interrupt():中断这个线程

isInterrupted(): 返回boolean 测试当前线程是否中断

isAlive():该线程是否处于活动状态

isDaemon():判断该线程是否是守护线程

setDaemon():设置该线程是否是守护线程

join() :合并线程,使它变为单线程

sleep(ms) :让当前线程休眠 ,休眠时间到了,自动唤醒

yield(): 让出cpu资源,使当前线程处理可运行状态(可运行状态也随时可以获取cpu资源)

案例1: 测试线程的基本属性

System.out.println("当前主线程:"+Thread.currentThread().getName());
            System.out.println("主线程id:"+Thread.currentThread().getId());
            //设置主线程的线程级别
            Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
            System.out.println("主线程的线程级别:"+Thread.currentThread().getPriority());
    
              //  创建线程对象
             MyThread my = new MyThread();
             //设置线程名
            my.setName("线程A");
            //设置优先级
            my.setPriority(10);
             my.start();
    
            MyThread my1 = new MyThread();
            my1.setName("线程B");
            //设置优先级     线程A  获取到资源的概率 大于线程B (大概率线程A优先执行完)
            my1.setPriority(1);
            //新生态
            System.out.println("线程"+my1.getName()+"状态-----"+my1.getState());
            my1.start();
            //可运行状态(就绪)
            System.out.println("线程"+my1.getName()+"状态-----"+my1.getState());
    
    
             for(int i = 0;i<100;i++){
                 System.out.println("主线程------"+i);
             }
    
守护线程

案例2: 守护线程

线程类型分为两种 一种是用户线程一种是守护线程,用户线程是执行某一个任务的独立代码 ,守护线程是用于守护用户线程的线程, 它的特点是 当用户线程执行完毕后守护现在自动结束,当用户线程没有执行完, 守护线程也不会停止

   操作系统中有守护进程 ,用于操作系统的运行,只有关机进程自动结束,这里守护线程和守护进程类似。

           //创建线程对象
            DaemonThread daemonThread = new DaemonThread();
            //设置该线程为守护线程   守护的是与它并行的线程类 ,当主线程或其他线程执行完毕,守护线程自动结束
           // daemonThread.setDaemon(true);
            System.out.println("是否是守护线程:"+daemonThread.isDaemon());
            daemonThread.start();
    
            for(int i=0;i<100;i++){
                System.out.println("主线程i------"+i);
            }

   活动的线程总数:  Thread.activeCount()  
线程中断

案例3: 关于终止线程

线程中止就是当线程运行时由于满足特定的条件需要停止运行,此时我们需要考虑如何安全的中止线程这里中止线程提供几个方法

方法1 : 打标记中断法
 线程运行1000,当程序达到500时,中止程序 

 

    public class ThreadEnd extends  Thread {
        @Override
        public void run() {
            boolean  isOver=false;
            for(int i = 0 ;i<1000;i++){
                if(i>=500){
                    isOver= true;
                    return ;
                }
                System.out.println("线程结果i-----------"+i);
            }
            System.out.println("正常结束");
        }
    
        public static void main(String[] args) {
              ThreadEnd th = new ThreadEnd();
              th.start();
        }
    }
    
方法2: 异常中断法
  • interrupt() :给线程打一个中断标记,不会立马中断  
    
  • interrupted() : 检测线程是否中断,并清除中断标记,返回boolean ,如果线程打标记了,就返回true
    
  • isInterrupted() : 检测线程是否中断,但不清除中断标记, 返回boolean  
    

注意用法: interrupted() : 它所处于的位置,对应于它作用的位置 ,通过线程类名调用

  interrupt() 和 isInterrupted() : 使用线程对象调用。 
   public class Thread1 extends  Thread {
        @Override
        public void run() {
            int i =0;
              while(true){
                  System.out.println("线程--------------"+i);
                   //判断当前线程是否有中断标记  ,但是不清除中断标记
                  if(this.isInterrupted()){
                      // 通过抛出异常或 break
                      System.out.println("当前线程打中断标记,可以停止了");
                      break;
                  }
                  i++;
              }
        }
    }
 public static void main(String[] args) throws InterruptedException {
             Thread1 th = new Thread1();
             th.start();
             // 休眠一会儿
            Thread.sleep(2000);
            //给th打中断标记
            System.out.println("打标记");
            th.interrupt(); //给th打标记
    
        }

3个方法的用法

     Thread.currentThread().interrupt();
            System.out.println("判断当前线程是否打标记 (清除标记):"+ Thread.interrupted());
            System.out.println("判断线程是否打标记(不清除标记)"+ Thread.currentThread().isInterrupted());
            System.out.println("判断当前线程是否打标记 (清除标记):"+ Thread.interrupted());  // 静态方法
      
join用法

案例四: join的用法: 合并当前线程 ,使其变为单线程 ,哪个线程调用join方法,就立即将该线程剩下的部分执行完成,再执行其他线程

    public class ThreadJoin extends  Thread {
        @Override
        public void run() {
                ThreadJoin2 th = new ThreadJoin2();
                th.setName("线程C");
                th.start();
    
               for(int i=0;i<100;i++){
                   try {
                       th.join();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println(Thread.currentThread().getName()+"-----"+i);
               }
        }
    }

        public static void main(String[] args) throws InterruptedException {
             ThreadJoin threadJoin = new ThreadJoin();
            threadJoin.setName("线程A");
             threadJoin.start();
    
    
    //        ThreadJoin threadJoin2 = new ThreadJoin();
    //        threadJoin2.setName("线程B");
    //        threadJoin2.start();
    
    
             for(int i=0;i<100 ;i++){
                   if(i==50){
                       // 合并线程 (threadJoin线程的所有代码合并到 主线程中,先执行threadJoin线程)
                       threadJoin.join();
                   }
    //               if(i==70){
    //                   threadJoin2.join();
    //               }
                 System.out.println("main---"+i);
             }
        }
sleep用法

案例五: sleep的用法: 用于休闲当前线程 ,休眠时间结束后自动唤醒继续执行,如果同时有多个线程执行 ,如果线程没有同步的情况下,相互休眠不影响,资源被公用。

    public static void main(String[] args) {
    
    for(int i =0;i<10;i++){
                try {
                    //让当前线程休眠200毫秒  200毫秒后自动唤醒线程 继续执行
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(i);
            }
            }

    class ThreadSleep implements  Runnable{
    
        @Override
        public void run() {
              for(int i =0;i<100;i++){
                  try {
                      Thread.sleep(1000); // 当前线程休眠时 不影响其他线程执行
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName()+"----"+i);
              }
        }
    }

            ThreadSleep obj = new ThreadSleep();
            Thread th1 = new Thread(obj , "线程A");
            Thread th2 = new Thread(obj , "线程B");
            th1.start();
            th2.start();
    
yield用法

案例六:yield的用法 : 出让cpu, 让当先线程变为可运行状态 ,并也可以继续抢占cpu资源

public static void main(String[] args) {
            ThreadYiled th = new ThreadYiled();
            th.start();
              // yield  让出cpu资源
            for(int i = 0;i<100;i++){
                    if(i==50){
                        //主线程让cpu
                        System.out.println("让出cpu");
                        Thread.currentThread().yield();
    
                    }
                System.out.println("主线程----"+i);
            }
        }
    

4、线程同步

线程并发场景:

在实际开发中,很多时候会出现多个线程同时访问同一个内存空间(变量)的场景,当他们同时对数据进行更新时,可能会出现数据不安全问题 ,例如经典的银行取钱案例

假设有一个账户(Account) ,余额是1100元,有小明和小明的爸爸同时取钱,小明拿着银行卡取ATM机取钱,小明的爸爸拿着存折取柜台取钱 ,他们都需要取1000块,小明在取钱时 系统会判断是否余额大于1000,如果大于,可以取钱,由于取钱需要一个过程,此时正好小明的爸爸也对该账户取1000,由于小明没有完成取钱的操作,卡里的钱还没有及时更新提交,所以小明的爸爸也可以通过系统判断的验证, 余额也大于1000,小明的爸爸也可以取钱,所以现在可能会出现他们两个人都取出1000元,导致账户数据不完整,这就是线程编发导致的问题
在这里插入图片描述
使用代码模拟场景

1、先有一个账户 (卡号,余额)

2、取钱的任务 ,由于需要使用同一个账户 ,这里的任务中有一个相同的账户 。

同步的解决办法:
1、将需要操作公共资源的代码增加 “同步锁” (也叫互斥锁)

语法:

synchronized(对象锁){
    代码块
}

注意这里的对象锁必须满足 两个线程是同一个对象(同一把锁)

   public void run() {
        System.out.println("开始取钱了");
        // 增加互斥锁,协同步伐  ,这个锁必须是公有的对象
        synchronized(account) {
            //先判断账户余额是否足够
            if (account.getMoney() >= 1000) {
                System.out.println(Thread.currentThread().getName() + "可以取钱");
                System.out.println(Thread.currentThread().getName() + "正在取钱");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //更新账户余额
                account.setMoney(account.getMoney() - 1000);
                System.out.println(Thread.currentThread().getName() +
                        "取到了钱,卡里余额还剩:" + account.getMoney());
            } else {
                System.out.println("抱歉,卡里余额不足,不能取1000元");
            }
        }

        // 以上代码需要将操作通过资源的代码块 增加同步关键字
    }
2、 同步方法

在方法的返回值前面增加 “synchronize” , 此时的锁代表的是当前this对象

 public synchronized void  get(){
        //先判断账户余额是否足够
        if (account.getMoney() >= 1000) {
            System.out.println(Thread.currentThread().getName() + "可以取钱");
            System.out.println(Thread.currentThread().getName() + "正在取钱");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //更新账户余额
            account.setMoney(account.getMoney() - 1000);
            System.out.println(Thread.currentThread().getName() +
                    "取到了钱,卡里余额还剩:" + account.getMoney());
        } else {
            System.out.println("抱歉,卡里余额不足,不能取1000元");
        }
    }

同步代码块和同步方法的区别:

语法不同,同步代码块更灵活,可以自定义锁对象 ,而同步方法不可以指定锁对象

5、线程通信

线程死锁的产生

​ 线程同步可以帮助我们解决多个线程操作同一个资源而导致数据不安全的问题,但线程同步也有可能产生隐患,假如一个线程中出现多个锁对象时,可能出现锁使用不当,导致锁与锁之前相互等待对方释放资源,从而形成一种 “相互等待”的僵局,这就是线程死锁。 例如哲学家吃饭

​ 模拟线程死锁

public class DeadThread implements  Runnable {
    Object obj1 = new Object();
    Object obj2 = new Object();
    @Override
    public void run() {
        // 模拟线程死锁
        // 如果当前线程为线程A  先拿到obj1锁   ,等待obj2锁资源
        // 如果当前线程为线程B  先拿到obj2锁  ,等待obj1锁的资源
        if(Thread.currentThread().getName().equals("线程A")){
             synchronized (obj1){
                 System.out.println(Thread.currentThread().getName()+"拿到了obj1的锁");
                 try {
                     Thread.sleep(500);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 System.out.println("我在等待obj2锁。。。。。");
                 synchronized (obj2){
                     System.out.println("我已经拿到了obj2锁。。。。");
                 }
             }
        }else{
            //线程B
            synchronized (obj2){
                System.out.println(Thread.currentThread().getName()+"拿到了obj2的锁");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("我在等待obj1锁。。。。");
                synchronized (obj1){
                    System.out.println("我已经拿到了obj1锁,我和开心");
                }
            }
        }

    }
}

    public static void main(String[] args) {
        DeadThread dead = new DeadThread();
        Thread th1 = new Thread(dead,"线程A");
        Thread th2 = new Thread(dead,"线程B");
        th1.start();
        th2.start();
    }

之所以会发生死锁,因为对象锁直接没有良好的“沟通”,导致互相获取对方的锁 ,进入等待中 ,可以通过线程类的几个方法 解决线程之间通信问题

线程通信的几个方法

​ wait() : 让当前线程处于等待中,会释放对象锁 ,但是不会自动唤醒,需要其他线程唤醒

​ notify() : 唤醒等待中的一个线程

​ notifyAll: 唤醒所有等待中的线程

他们都属性Object的方法,需要相同的对象 ,使用时 通过Object的对象调用

注意: 以上方法的调用必须满足两个条件: a、他们必须在同步代码块中执行, b、调用该方法的对象是锁对象

案例1:模拟3个人,张飞、李逵和刘备,来买电影票,售票员只有一张5元的钱,电影票5元钱一张。

  • 张飞拿20元一张的人民币排在李逵和刘备的前面,李逵和刘备各拿了一张5元的人民币买票。

    
     */
    public class TicketThread implements  Runnable{
        //公共资源:
        int fiveCount=1;
        int twentyCount =0;
    
    
        @Override
        public void run() {
            //开始买票  如果是张飞,他是20元面值,其他都是5元
            if(Thread.currentThread().getName().equals("张飞")){
                //20元面值买票
                takeTicket(20);
            }else{
                // 5元面值买票
                takeTicket(5);
            }
    
        }
    
        /**
         * 买票过程   给方法加同步  ,锁对象默认是 方法
         * @param money
         */
        public synchronized void  takeTicket(int money){
            if(money ==20){
                // 验证 当前公共资源 中是否有3张5元
                 while(fiveCount<3){
                     //等待
                     System.out.println(Thread.currentThread().getName()+"不能买到票,要继续等待");
                     try {
                         this.wait();
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
                 // 程序执行这里,说明 fiveCount >=3
                System.out.println(Thread.currentThread().getName()+"正在买票。。");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                fiveCount-=3;
                twentyCount++;
                System.out.println(Thread.currentThread().getName()+"已经买到了票。。");
    
            }else{
                System.out.println(Thread.currentThread().getName()+"正在买票。。");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                fiveCount++;
                System.out.println(Thread.currentThread().getName()+"已经买到了票。。");
                // 唤醒等待的线程
                this.notify();
            }
        }
    
    }
    
    
      public static void main(String[] args) {
             TicketThread ticketThread = new TicketThread();
             Thread th1 = new Thread(ticketThread,"张飞");
            Thread th2 = new Thread(ticketThread,"李逵");
            Thread th3 = new Thread(ticketThread,"刘备");
            //开启线程
            th1.start();
            th2.start();
            th3.start();
        }
    
线程的生产者和消费者模式
 多个线程同时运行时,会产生线程并发可使用同步操作确保数据的安全性,如果需要各线程之间交互,可是使用线程等待和唤醒模式,在这里常用的等待唤醒中经典的模式为“生产者和消费者模式” 

 生产者和消费者由两类线程组成:  若干个生产者线程 负责提交用户的请求,若干个消费者线程负责处理生成出来的任务。 他们操作一块共享内存区进行数据通信。

在这里插入图片描述

生成/消费的产品(数据): Mobile (手机编号)

生成者线程类: Provider : 无限制的生成手机

消费者线程类:Customer : 无限制的消费手机

​ 共享存储区: Storage ( push 、pop) 存储手机的对象数组

​ 测试类

public class Mobile {
    private int id;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Mobile(int id) {
        this.id = id;
    }
}

存储区

package com.j2008.provider_customer;

/**
 * ClassName: Storage
 * Description:
 * date: 2020/11/10 15:32
 *  存储区,它是生产者和消费者共享的空间 ( 生产者和消费者将该对象作为公有锁)
 * @author wuyafeng
 * @version 1.0   softeem.com
 */
public class Storage {

    // 定义存储手机的对象数据
    Mobile [] mobiles = new Mobile[10];
    int index=0;  // 个数

    static int n=1000;
    /**
     * 存放手机
     * @param mobile
     */
    public synchronized void push(Mobile mobile){
        //考虑容器上限已满,必须等待
        while(index == mobiles.length){
            System.out.println("容器已满,需等待");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //通知消费者取消费 ,将其他线程全部唤醒
        this.notifyAll();
        mobiles[index]=mobile;
        index++;

    }

    /**
     * 取出手机      1 2 3 4
     *              1 2 3
     *              index--;
     *             mobile[index] =null
     * @return 取出的手机对象
     */
    public synchronized Mobile pop(){
        Mobile m = null;

        // 判断index是否小于0
        while(index<=0){
            //等待
            System.out.println("容器中没有手机,需要等待");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        index--;
        m = mobiles[index];
        //将容器中的这个位置 设置为空
        mobiles[index]=null;
        // 通知生产者去生产
        this.notifyAll();
        return m;

    }

    public synchronized int getSize(){
        return index;
    }

}

生产者:

package com.j2008.provider_customer;

/**
 * ClassName: Provider
 * Description:
 * date: 2020/11/10 15:54
 *
 * @author wuyafeng
 * @version 1.0   softeem.com
 */
public class Provider implements  Runnable {
    //共享存储区
    Storage storage =null;
    public Provider(Storage storage){
        this.storage = storage;
    }


    @Override
    public void run() {
        //手机编号
        int n=1000;
        //一直生产
        while(true){
            Mobile m = new Mobile(n);
            storage.push(m);
            System.out.println(Thread.currentThread().getName()+
                    "生产了一部手机,其编号:"+m.getId()+" 其库存:"+storage.getSize());

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            n++;
        }
    }
}

消费者

package com.j2008.provider_customer;

/**
 * ClassName: Customer
 * Description:
 * date: 2020/11/10 15:58
 *
 * @author wuyafeng
 * @version 1.0   softeem.com
 */
public class Customer implements  Runnable {

    Storage storage=null;
    public Customer(Storage storage){
        this.storage = storage;
    }

    @Override
    public void run() {
        while(true){
            Mobile mobile = storage.pop();
            System.out.println(Thread.currentThread().getName()+
                    "消费了一部手机,编号》》"+mobile.getId()+" 库存:"+storage.getSize());
            try {
                Thread.sleep(1000