Java基础,学完就可以学框架了

渡星河
2023-01-12 / 0 评论 / 28 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2023年02月02日,已超过595天没有更新,若内容或图片失效,请留言反馈。

一、计算机语言

  • 语言:是人与人之间用于沟通的一种方式。例如:中国人与中国人用普通话沟通。而 中国人要和英国人交流,就要学习英语。
  • 计算机语言:人与计算机交流的方式。 如果人要与计算机交流,那么就要学习计算机语言。 计算机语言有很多种。如:C ,C++ ,Java ,Python, Go, Ruby等。

1、编程语言的发展

  • 第一代语言:机器语言,程序使用原始的01010110101编写。
  • 第二代语言:汇编语言,使用助记符(因为字符和符号)编写程序
void multstore (long x,long y, long *dest) {
    long t = mult2(x, y) ;
    *dest=t;
}

img

  • 第三代语言:高级语言,代表语言有:C, C++, Java, PHP, Python, Go, Rust, Ruby, C#等

下图简要介绍了编程语言的发展

img

2、为什么选择Java?

  • 就业率
  • 市面上应用型系统几乎都是java开发

二、Java快速入门

1、Java语言的发展(了解)

1991年4月,由James Gosling博士领导的绿色计划(Green Project)开始启动,此计划最初的目标是开发一种能够在各种消费性电子产品(如机顶盒、冰箱、收音机等)上运行的程序架构。这个计划的产品就是Java语言的前身:Oak(得名于James Gosling办公室外的一棵橡树)。Oak当时在消费品市场上并不算成功,但随着1995年互联网潮流的兴起,Oak迅速找到了最适合自己发展的市场定位并蜕变成为Java语言。

img

2、Java技术体系平台

  • JavaSE:包括了Java核心API,也是我们第一阶段的重点
  • Java EE:企业级开发,包含Servlet, Jsp等,主要用于web开发,第二阶段的学习重点
  • Java ME:主要运行在移动终端,如手机,Pad等。
  • Java Card:运行在小内存设备上,如智能门锁。

3、Java应用领域

  • 企业级应用:主要有企业级软件(如银行系统,OA系统,政务系统等)、各种类型的网站后台服务(如淘宝,京东等)。Java的安全机制以及 它的跨平台的优势,使它在分布式系统领域开发中有广泛应用。应用领域包括金融、电 信、交通、电子商务等。
  • Android平台应用:Android应用程序使用Java语言编写。Android开发水平的高低 很大程度上取决于Java语言核心能力是否扎实。
  • 大数据平台开发:各类框架有Hadoop,spark,storm,flink等,就这类技术生态 圈来讲,还有各种中间件如flume,kafka等等 ,这些框架以及工具大多数 是用Java编写而成,但提供诸如Java,scala,Python,R等各种语言API供编程。
  • 移动领域应用:主要表现在消费和嵌入式领域,是指在各种小型设备上的应用,包括手 机、PDA、机顶盒、汽车通信设备等。

4、Java语言的特性

  • 面向对象:Java是一个面向对象的语言。
  • 跨平台(一处编译,到处执行):Java编译成字节码,然后通过解释器运行。这一特点让Java具备跨平台特性。
  • 安全性高:Java是一款安全性相对较高的语言,比如去掉了指针,这一就避免伪造指针操作内存,进而提升了安全性。
  • 简易学:去掉了指针、多继承、手动垃圾回收等,对于开发人员非常友好、简单、使得学习成本降低。
  • 多线程:提供更加简易的实现多线程的编写方式。

5、Java各版本介绍以及我们该使用哪个版本

img

6、JDK安装配置

检查本机是否安装java环境:通过打开windows的dos窗口,然后输入java,回车之后出现以下信息表示本机没有Java环境

img

安装java环境步骤:

第一步:去官网: https://www.oracle.com/java/technologies/downloads/ 下载对应的jdk版本

以下载jdk11为例:

img

下载时所需账号密码:账号: 13707259624@163.com 密码:Java123456

第二步:双击开始安装Java环境,一致点击下一步即可,唯一点就是记录自己安装时选择的安装目录。

第三步:安装完毕,再次打开DOS窗口,输入命令java,显示以下信息表示java环境安装成功

img

理论上有了java环境我们就可以编写java程序。编写完成之后就可以运行该程序,但是在编译java程序时出现以下信息,该信息是由于安装java环境后没有配置环境变量所致。所以需要配置环境变量,让系统找到avac编译器。

img

windows配置java环境变量步骤

在本机上找到以下内容

img

此时就可以在环境变量窗口配置java环境变量

img

把JAVA_HOME添加到path里

img

添加完path后点击下图中的确定按钮

img

img

最后关闭之前打开dos窗口重新打开。输入javac,如果有如下信息则表示本机已经可以使用javac编译器

img

有了环境我们就可以真正开始编写java程序。

7、第一个java程序

//第一个java程序
//public公共的,表示其他java程序可以访问到该类
//class  类
//HelloWorld 类名
public class HelloWorld {
    //以下代码片段叫做方法,方法名字叫做 main,所有的java程序,入口都叫main。
    //main之后括号内的内容叫做  参数,String表示参数类型,args表示参数名。
    //public static void 是java语法规定的关键字,此处可以暂时按照字面意思理解。
    public static void main(String[] args) {
        //System是java自带的一个类,也可以叫做工具。作用是用于在控制台输出内容。
        //println()是System.out下的一个方法,里边被英文输入法下双引号包裹的内容叫做参数,也叫字符串。
        //以下内容先背下来。 
        System.out.println("Hello World!");
    }
}

怎么运行java程序

img

第一个程序需要注意的点:一个java文件建议只写一个类,并且该类使用public修饰。一个类会对应生成一个class文件。

6、介绍jdk、jre、jvm三者之间的关系。

img

java里的注释

注释:写道程序里,对程序逻辑进行描述,但是不会参与程序的运行

java里有三种注释方式:单行注释,多行注释,文档注释。

DOS常用命令:

切换盘符:磁盘名: 进行切换,如要切换到d磁盘 d:或D:

img

进入文件夹:cd 文件夹名

img

回到上级目录:cd ..

img

列出当前目录下有哪些文件:dir

img

清屏命令:cls

  • 常用的DOS命令列表

    • dir : 列出当前目录下的文件以及文件夹
    • md : 创建目录
    • rd : 删除目录
    • cd : 进入指定目录
    • cd.. : 退回到上一级目录
    • cd\: 退回到根目录
    • del : 删除文件
    • exit : 退出 dos 命令行
    • 补充:echo javase>1.doc
  • 常用快捷键

    • ← →:移动光标
    • ↑ ↓:调阅历史操作命令
    • Delete和Backspace:删除字符

8、开发工具的安装:notepad++

9、Homework

  1. 编写第一个HelloWorld程序,并给程序加上注释说明程序的作用
  2. 按照身份证的上的格式,输出个人信息。
  3. 编写一个程序,在控制台打印输出一下图案

img

public static void main(String[] args) {
    System.out.println("千千千千千千        锋   教            教    育");
    System.out.println("   千千          锋锋    教          教    育育");
    System.out.println("   千千         锋  锋    教        教    育  育");
    System.out.println("   千千        锋    锋    教      教    育    育");
    System.out.println("   千千       锋锋锋锋锋锋    教    教    育育育育育育");
    System.out.println("千  千千      锋        锋    教  教    育        育");
    System.out.println("千  千千     锋          锋    教教    育          育");
    System.out.println("千千千千    锋            锋    教    育            育");
}

三、Java语法基础

1、变量

1.1 标识符

命名规范,可以是字母、数字、下划线等字符,但是不能以数字开头。标识符的取名要做到见名知意。

1.2 变量概念

为了操作数据,需要把数据存放到内存中。所谓内存在程序看来就是一块有地址编号的连续的空间,数据放到内存中的某个位置后,为了方便地找到和操作这个数据,需要给这个位置起一个名字。编程语言通过变量这个概念来表示这个过程。

声明一个变量,比如int a,其实就是在内存中分配了一块空间,这块空间存放int数据类型,a指向这块内存空间所在的位置,通过对a操作即可操作a指向的内存空间,比如a=5这个操作即可将a指向的内存空间的值改为5。

之所以叫“变”量,是因为它表示的是内存中的位置,这个位置存放的值是可以变化的。

1.3 变量名

命名规范,可以是字母、数字、下划线等字符,但是不能以数字开头。标识符的取名要做到见名知意。

1.2 变量类型

类型决定了操作系统需要给这个变量分配多大的内存空间。

1.3 变量的值

变量值就是具体存储到内存空间中的值。

1.4 变量的类型

1.4.1 局部变量

定义在方法体中,或方法参数中,以及代码块中的变量。

1.4.2 全局变量

1.4.2 .1 静态变量

定义在方法或代码块之外,并且使用关键字static 修饰的变量,静态变量属于类。

1.4.2.2 成员变量

定义在方法或代码块之外的变量,属于对象,需要通过创建对象,然后适用对象的引用去访问该变量。

/*
变量的使用
*/
//要写一个java程序,第一就是新建一个类
public class Variable {
  //定义静态变量
  static int b = 123;
  //定义成员变量
  int c = 234;
  //编写程序入口main方法
  public static void main(String[] args) {
    //定义一个变量,变量的三要素:数据类型、变量名、变量值
    int a = 100;
    System.out.println(100);//在屏幕输出具体的值
    System.out.println(a);//输出变量

    //获取静态变量的方式,通过  “类名.静态变量名”  的形式访问
    System.out.println(Variable.b);
    //应为b属于类Variable,而我们的main也是写在Variable类中,所以访问该静态变量可以省略前边的类名
    System.out.println(b);

    //访问非静态的全局变量,需要通过对象才能访问
    //首先需要通过关键字new创建包含了这个变量的类的一个对象,
    Variable vr = new Variable();
    System.out.println(vr.c);
  }
}

演示变量的作用域:

/*
变量的使用
*/
//要写一个java程序,第一就是新建一个类
public class Variable {
  //定义静态变量
  static int b = 123;
  //定义成员变量
  int c = 234;
  //编写程序入口main方法
  public static void main(String[] args) {
    //定义一个变量,变量的三要素:数据类型、变量名、变量值
    int a = 100;
    System.out.println(100);//在屏幕输出具体的值
    System.out.println(a);//输出变量

    //获取静态变量的方式,通过  “类名.静态变量名”  的形式访问
    System.out.println(Variable.b);
    //应为b属于类Variable,而我们的main也是写在Variable类中,所以访问该静态变量可以省略前边的类名
    System.out.println(b);

    //访问非静态的全局变量,首先需要通过关键字new创建包含了这个变量的类的一个对象,
    Variable vr = new Variable();
    System.out.println(vr.c);

    //访问类 V 里的变量 abc 和  v1
    //访问 abc,使用 类名.变量名
    System.out.println("V的静态变量abc: " + V.abc);

    //访问 v1, 因为是非静态成员变量,所以需要基于类V创建对象
    V v = new V();
    System.out.println("V的非静态变量v1: " + v.v1);//加号表示把字符串和后边变量的值拼接起来。

    //如果现在类Variable里访问 V类里的man方法中的ss变量,是没有办法可以访问的。
    //如果man方法和我们的main方法处在同一个类中,此时想要访问man方法中的ss变量也是不能访问到的。
  }

  //定义一个方法,并且在方法内定义一个局部变量
  public static void man() {
    //局部变量
    int ss = 132;
  }
}

//定义一个新类。这种方式不推荐,一个类我们是建议定义在一个单独的文件中,此处为了演示
class V {
  //定义一个静态变量
  public static int abc = 120;
  //定义一个成员变量
  public int v1 = 520;

  //定义一个方法,并且在方法内定义一个局部变量
  public static void man() {
    //局部变量
    int ss = 132;
  }
}

注意:关于变量的作用域会在讲解方法章节做具体介绍。

2、数据类型

数据类型用于对数据归类,以便于理解和操作。对Java语言而言,有如下基本数据类型。

世界万物都是由元素周期表中的基本元素组成的,基本数据类型就相当于化学中的基本元素,而对象就相当于世界万物。

byte : 字节,1个字节, 8位

short :短整型,2个字节,16位

int : 整型, 4个字节, 32位

long : 长整型, 8个字节,64位

float : 浮点型, 4个字节,32位

double :浮点型, 8个字节,64位

char : 字符,2个字节, 16位

boolean : 布尔, 根据实际场景确定占一个字节或者int类型的大小

  1. boolean 类型被编译成 int 类型来使用,占 4 个 byte 。
  2. boolean 数组被编译成 byte 数组类型,每个 boolean 数组成员占 1 个 byte 。
  3. 在 Java 虚拟机里,1 表示 true ,0 表示 false 。
  4. 这只是 Java 虚拟机的建议。

    1. img
  5. 可以肯定的是,不会是 1 个 bit 。

2.1 整数

有4种整型byte、short、int、long,分别有不同的取值范围

2.2 浮点数(小数)

有两种类型单精度浮点数:float

双精度浮点数:double

有不同的取值范围和精度

2.3 字符

char,表示单个字符

2.4 布尔

boolean,表示真(true)或假(false),当变量定义为boolean类型时占用四个字节,当定义布尔数组时数组内的每一个布尔元素占一个字节。

八种基本数据类型定义变量代码演示:

/*
数据类型
*/
//定义一个新类
public class DataType {
  //编写程序入口main方法
  public static void main(String[] args) {
    //定义一个布尔类型的变量
    boolean a = true;
    System.out.println("布尔变量:a = " + a);
    //定义字符类型的变量
    //字符和字符串的区别:字符串属于引用类型,字符属于基本数据类型,定义方式也不一样,字符串使用双引号,字符使用单引号。
    char ch = 'A';
    //char ch1 = 'abc';//会报错
    char ch2 = 64;
    System.out.println("字符 ch = " + ch);
    System.out.println("字符 ch2 = " + ch2);

    //定义整数类型
    byte b = 123;
    short s = 234;
    int i = 102;
    long l = 888L;
    System.out.println("b = " + b + "  s = " + s  +  "  i = " + i + "  l = " + l);

    //定义小数,也叫做浮点数。
    float f = 1.0F;//单精度
    double d = 1.0;//双精度
    System.out.println("f = " + f + "  d = " + d);
  }
}

2.5 转义字符

java里在一些字符前加上“\"来表示转义字符。

/*转义字符*/
//定义类
public class EscapeCharacter {
  //程序入口
  public static void main(String[] args) {
    //用字符表示换行 \n
    System.out.println("Hello \n World");
    //制表符 \t
    System.out.println("\tHello World");
    //双引号 \"\"
    System.out.println("\"Hello World\"");
    char ch = 7;//响铃
    System.out.println(ch);
  }
}

2.6 字符集

常用字符集

ascii 1(byte) 0 ~127 latin1 1(byte) 128 ~ 255 兼容ascii gb2312 2(byte) 兼容ascii gbk 2(byte) 兼容ascii utf-8 3(byte),utf-8字符集是Unicode编码标准的一个具体实现 兼容ascii

练习

定义8中基本数据类型的变量,不要给它们赋值,然后打印查看其默认值

3、数据的表示与和处理

在十进制中,如:123,可以表示为:1 10^2 + 2 10^1 + 3 * 10^0,位权上的值从右到左分别是10^0, 10^1, 10^2.....10^n-1。数值位上的值有十种可能0~9。

在二进制中,如:1010,可以表示为:12^3 + 02^2 + 12^1 + 02^0 = 8 + 0 + 2 + 1 = 10;

位权上和十进制类似。只是数值位上的值只有两种0或1。

3.1 整数

3.1.1 正数

  • 十进制与二进制转换

    • 方法:通过除2取余后倒序排列即可得到十进制的二进制
    • 例:十进制13,计算二进制?
    • ①13除2,商是6,余数是1;
    • ②6除2,商是3,余数是0;
    • ③3除2,商是1,余数是1;
    • ④1除2,商是0,余数是1。
    • 倒序排列余数④③②①---> 1101
  • 二进制转十进制

二进制:1010;

十进制:1*2^3 + 0*2^2 + 1*2^1 + 0*2^0 = 8 + 0 + 2 + 1 = 10;

  • 二进制转十六进制

二进制四个位对应十六进制一位,十六进制表示法都有一个前缀:0x;用数字和字母0-F表示0-15,其中0-9对应十进制0-9,10-15对应A-F,字母不区分大小写。

二进制:1010 0011;

十六进制:0xA3 或 0xa3

  • 十六进制转二进制
  • 其他进制之间的转换

    • 十进制转十六进制通过除16取余数
    • 建议把十进制转成二进制直接通过二进制转成16
    • 二进制每四位对应一位十六进制,二进制每三位对应一位八进制

二进制、十进制和十六进制转换表:

img

练习

将0x39A7F8转换为二进制 :0011 1001 1010 0111 1111 1000

将二进制1100 1001 0111 1011转换为十六进制0xC97B

3.1.2 负数

十进制表示负数直接在数值前加一个符号“-”即可。例如123,负数就是 -123,但二进制如何表示负数呢?其实概念是类似的,二进制使用最高位表示符号位,用1表示负数,用0表示正数。

但哪个是最高位呢?整数有4种类型byte、short、int、long,分别占1、2、4、8个字节,即分别占8、16、32、64位,每种类型的符号位都是其最左边的一位。为方便举例,下面假定类型是byte,即从右到左的第8位表示符号位。

但负数表示不是简单地将最高位变为1,比如:

  • byte a=-1,如果只是将最高位变为1,二进制应该是10000001,但实际上,它应该是11111111。
  • byte a=-127,如果只是将最高位变为1,二进制应该是11111111,但实际上,它却应该是10000001。

和我们的直觉正好相反,这是什么表示法?这种表示法称为补码表示法,而符合我们直觉的表示称为原码表示法,补码表示就是在原码表示的基础上取反然后加1。取反就是将0变为1,1变为0。负数的二进制表示就是对应的正数的补码表示,

  • 十进制转二进制

    • -1:1的原码表示是00000001,取反是11111110,然后再加1,就是11111111。
    • -2:2的原码表示是00000010,取反是11111101,然后再加1,就是11111110。
    • -127:127的原码表示是01111111,取反是10000000,然后再加1,就是10000001。
  • 二进制转十进制
  • 给定一个负数的二进制表示,要想知道它的十进制值,可以采用相同的补码运算。
    1. 10010010,首先取反,变为01101101,然后加1,结果为01101110,它的十进制值为110,所以原值就是-110。

直觉上,应该是先减1,然后再取反,但计算机只能做加法,而补码的一个良好特性就是,对负数的补码表示做补码运算就可以得到其对应正数的原码,正如十进制运算中负负得正一样。

所以一个byte型的数据能表示的范围是:1000 0000 到 0111 1111 ----> -128 到 127,其他数据类型类似

3.2 整数二进制运算

计算机是只能做加法的,1 - 1 会转成 1 + (-1)

用符合直觉的原码表示,1-1的结果是-2,计算结果是不对的。

img

换成补码表示,结果就是0

img

理解了二进制的加减之后我们就能解释为什么在程序里两个数相加减会得到一个预料外的值

System.out.println((byte)(127 + 1));//-128

img

正数的,原码和补码反码都是同一个数。原码取反就是等于反码,反码加1可得补码。

3.3 小数的二进制表示(了解)

小数,在编程语言里又叫浮点数。为什么叫浮点数。

为什么要叫浮点数呢?这是由于小数的二进制表示中,表示那个小数点的时候,点不是固定的,而是浮动的。

比如:

float f = 0.1f * 0.1f;
System.out.println(f);

结果是0.010000001,为什么是这样?

这里并不是计算机计算出了错,而是计算机本来就没法精确的表示许多小数。

同样,在我们的十进制的世界里,也存在不能精确表示的小数,如三分之一。

那么哪些小数是可以精确表示的?

  • 十进制:可以表述为10的多少次方和的数就能精确表示,比如12.345,实际上表示的是1× 10+2× 1+3× 0.1+4× 0.01+5× 0.001
  • 二进制:可以精确表示为2的某次方之和的数可以精确表示,其他数则不能精确表示

img

java里浮点数的表示遵循 IEEE-754标准,其中单精度和双精度使用不同位表示,如下

img

使用一个单精度浮点数列子来说明:

十进制小数 123.456,使用科学计数法可以写成 1.23456 * 10的二次方

同理,二进制也可以使用类似方法表示一个数,如十进制小数:7.125,它的二进制小数等于0111.001,因为计算机不能存储点,所以肯定不能把0111.001存储到计算机中。我们使用类似十进制的科学计数法表示二进制0111.001可以写成1.11001 * 2^2。

根据上图,s(sign)表示符号位,而7.125是正数,所以,s为0,1.11001 * 2^2表达式中2的阶是2,所以exp=基数+阶码=127 + 2 = 129,129二进制为:10000001。最后尾数部分frac,就是二进制小数点右边的数,此处是11001,frac占23位,11001占5位,就在后面补18个0,最后frac=11001000000000000000000

最后7.125保存在计算机中的二进制为 s + exp + frac = 0 10000001 11001000000000000000000

4、赋值

声明变量之后,就在内存分配了一块位置,但这个位置的内容是未知的,赋值就是把这块位置的内容设为一个确定的值。Java中基本类型、数组、对象的赋值是不同的,本节介绍基本类型的赋值,

4.1 整数类型赋值(byte/short/int/long)

byte b = 23;
short s = 3333;
int i = 8888;
long l = 32323;

但是,在给long类型赋值时,如果常量超过了int的表示范围,需要在常量后面加大写或小写字母L,即L或l,例如:

long a = 3232343433L;

之所以需要加L或l,是因为数字常量默认为是int类型。

4.2 小数类型赋值(float/double)

对于double,直接把熟悉的小数表示赋值给变量即可

double d = 999.99;

但对于float,需要在数字后面加大写字母F或小写字母f,这是由于小数常量默认是double类型。

float f = 999.99f;

4.3 字符类型赋值(char)

字符类型char用于表示一个字符,这个字符可以是中文字符,也可以是英文字符,char占用的内存空间是两个字节。赋值时把常量字符用单引号括起来,不要使用双引号;大部分的常用字符用一个char就可以表示,但有的特殊字符用一个char表示不了。

char c = 'A';
char z = '马';
char c1 = 39532;
char c2 = 0x9a6c;
char c3 = '\u9a6c';

第1种赋值方式是最常见的,将一个能用ASCII码表示的字符赋给一个字符变量。第2种赋值方式也很常见,但这里是个中文字符,需要注意的是,直接写字符常量的时候应该注意文件的编码,比如,GBK编码的代码文件按UTF-8打开,字符会变成乱码,赋值的时候是按当前的编码解读方式,将这个字符形式对应的Unicode编号值赋给变量,“马”对应的Unicode编号是39 532,所以第2种赋值方式和第3种赋值方式是一样的。第3种赋值方式是直接将十进制的常量赋给字符。第4种赋值方式是将十六进制常量赋给字符,第5种赋值方式是按Unicode字符形式。所以,第2、3、4、5种赋值方式都是一样的,本质都是将Unicode编号39 532赋给了字符。

4.4 真假类型赋值(boolean)

真假(boolean)类型很简单,直接使用true或false赋值,分别表示真和假

boolean b = true;
b = false;

4.5 其他赋值

4.5.1 把变量赋给变量

int a = 666;
int b = a;

4.5.2 将运算结果赋给变量

int a = 1;
int b = 2;
int c = 2 * a * b;

4.5.3 字符串赋值

String s = "Hello World";

4.6 强制类型转换

自动转换

从小到大会进行类型的自动升级,也叫做自动转换

/*低位到高位的类型转换*/
public class LowToHighCast {
  //定义程序入口
  public static void main(String[] args) {
    byte bt = 12;
    short st = bt;
    int it = st;//变量赋值给变量
    System.out.println(bt);
    System.out.println(st);
    System.out.println(it);
    float ft = st;
    System.out.println(ft);
  }
}

强制转换

把多字节的数据赋值给少字节的变量时会报错,因为存储不下,如果非要强制存储,此时就需要显示转换。

/*高位到低位的类型转换*/
public class HighToLowCast {
  //定义程序入口
  public static void main(String[] args) {
    int it = 256;//变量赋值给变量
    //括号里的byte就是告诉java虚拟机需要把it的数据类型转换成指定类型,转成的类型需要和接收的类型保持一致
    byte bt = (byte)it;
    short st = (short)it;
    System.out.println(bt);
    System.out.println(st);
    boolean bl = true;//布尔类型没有向上和向下的转型
    char cr = 'a';//因为字符最终会被保持为字符表里对应的整数,所以可以和表示整数的类型相互转换
    byte be = (byte)cr;
    System.out.println(be);
  }
}

4.7 练习题

​ 1、变量使用的三要素是?

​ 2、变量的命名规则包括几点?

​ 3、Java中常用的数据类型有哪些?

​ 4、Java中的数据类型转换包括哪两种?

​ 5、Java中的数据类型转需要注意哪些?

  • ​ 6、不转换为十进制或二进制计数下列表达式
  • 0x503c + 0x8 =
  • 0x503c - 0x40 =
  • 0x503c + 64 =
  • ​ 0x50ea - 0x503c =

5、算术运算

5.1 运算符优先级

img

5.2 加、减、乘、除,符号分别是+、-、*、/

/*算术运算符*/
public class ArithmeticOperator {
  public static void main(String[] args) {
    //+  int数据类型和int数据类型相加
    int a = 12;
    int b = 3;
    int c = a + b;//两个变量相加
    int d = a + 20;//变量和常量相加
    int e = 15 + 150;//常量与常量相加
    //被双引号包起来的部分会在dos窗口或控制台原样输出
    System.out.println("a + b = " + c);
    System.out.println("a + 20 = " + d);
    System.out.println("15 + 150 = " + e);
    //+  int数据类型和非int数据类型相加
    byte bt = 101;
    byte bt1 = (byte)(bt + a);//bt和a相加的结果是一个int类型,所以这里赋值给byte型需要强制类型转换
    System.out.println(a + bt);
    int it = bt + bt1;//因为bt和bt1都是byte,只占一个字节,相加后赋值给int型属于从低位到高位,会自动转换类型
    System.out.println(it);

    //+  int数据类型和浮点数数据类型相加
    float ft = 3.0F;
    //ft变量的值3.0F中的F千万不可省略,省略后相加结果会被当做double类型。省略后使用强制类型转换也会报错
    float ft1 = ft + a;
    System.out.println(ft1);

    //+ 加号能使用到布尔类型变量
    boolean bl = true;
    //int a = bl + 10;//会报错,不能用于布尔变量
    //+ 加号用于字符串,起拼接的作用
    String s = "hello";
    String s1 = "world";
    String s2 = s + s1;
    System.out.println(s2);
  }
}

以上代码演示了加号的使用,乘除减法的使用类似,只需要把以上代码里的+变成想要符号即可,但是对于字符串其他符号就不能使用。

注意:运算符中只有加号能够用于字符串,起着拼接左右字符串的作用,其他的算术运算不能用于字符串。

5.3 % 数学中的求余数

例如,5%3是2,10%5是0

//% 取余操作   7 除 2 等于 3,余数是 1
    System.out.println("7 % 2 = " + 7 % 2);
    int a = 15;
    int b = 6;
    int c = a % b;
    System.out.println(c);
    System.out.println(15.0F % 6.0F);

5.4 自增(++)/自减(--)

自增/自减是对自己做加1或减1操作,但每个都有两种形式,一种是放在变量后,例如a++、a--,另一种是放在变量前,例如++a、--a。如果只是对自己操作,这两种形式也没什么差别,区别在于还有其他操作的时候。放在变量后(a++)是先用原来的值进行其他操作,然后再对自己做修改,而放在变量前(++a)是先对自己做修改,再用修改后的值进行其他操作。

  // ++自增,当++出现在变量左边就是先自增在使用,出现右边先使用后自增  --自减和自增一样。
    int it = 1;
    System.out.println(++it);
    int it1 = 10;
    System.out.println(it1++);

    float ft = 3.1F;
    System.out.println(++ft);

    int it2 = 9;
    System.out.println(--it2);
    System.out.println(it2--);
    //System.out.println(--80);自增运算符不能直接使用到常量上
    //求下列程序的结果
    int x = 10; int y = 8;
    System.out.println(x++ - --x + y-- + x++ - --y);//12
    System.out.println(x);//11
    System.out.println(y);//6

在大多数计算机中,CPU运行时是以4个字节来处理数据的。因此当变量小于4个字节时,并且进行算数运算,会发生整型提升,这就是 byte c = a + b会报错的原因, byte d = 1 + 2不会报错是因为 1 和 2 是常量。

5.5 字符串中使用加号

 //+ 加号用于字符串,起拼接的作用
    String s = "hello";
    String s1 = "world";
    String s2 = s + s1;
    System.out.println(s2);

5.6 +=, -=, *=, /=, %=

/*算术运算符*/
public class ArithmeticOperator {
  public static void main(String[] args) {
    //+=加等于,  -=,  *=,  /=,  %=
    int a = 10;
    a += 5;//a = a + 5;
    System.out.println("a += 5 : " + a);
    int b = 20;
    b -= 21; // b = b - 21;
    System.out.println("b -= 21 : " + b);
    int c = 30;
    c *= 2;//c = c * 2;
    System.out.println("c *= 2 : " + c);
    int d = 40;
    d /= 20;//d = d / 20
    System.out.println("d /= 20 : " + d);
    int e = 15;
    e %= 7; //e = e % 7
    System.out.println("e %= 7 : " + e);
  }
}

5.7 加、减、乘、除注意事项

5.7.1 溢出

运算时要注意结果的范围,使用恰当的数据类型。两个正数都可以用int表示,但相乘的结果可能就会超出,超出后结果会令人困惑

int a = 2147483647 * 2;//2147483647是int能表示的最大值
    short a1 = 255;
    a1 += 1;
    byte c = (byte)(a1);
    System.out.println(c);

a的输出结果是-2,因为计算的结果超出了int能表示的范围,所以出现了溢出。为什么会是-2

如何避免这种溢出?

要避免这种情况,我们的结果类型应使用long,但只改为long也是不够的,因为运算还是默认按照int类型进行,需要将至少一个数据表示为long形式,即在后面加L或l,下面这样才会出现期望的结果

int a = 2147483647 * 2L;

5.7.2 舍弃

整数相除不是四舍五入,而是直接舍去小数位

double d = 8 / 5;//1.0 整数相除不是四舍五入,而是直接舍去小数位
    double d1 = 8.0 / 5.0;//1.6
    System.out.println(d);
    System.out.println(d1);

结果是2而不是2.5,如果要按小数进行运算,需要将至少一个数表示为小数形式,或者使用强制类型转化,即在数字前面加(double),表示将数字看作double类型,如下所示任意一种形式都可以

double d1 = 8 / 5.0;
double d2 = 8 / (double)5;

5.7.3 浮点数计算结果不精确

无论是使用float还是double,进行运算时都会出现一些非常令人困惑的现象

float f = 0.1f * 0.1f;
System.out.println(f);
double d = 0.1 * 0.1;
System.out.println(d);

输出是:

0.010000001

0.010000000000000002

这是怎么回事?看上去这么简单的运算,计算机计算的结果怎么不精确呢?但事实就是这样,究其原因,我们需要理解float和double的二进制表示。

6、关系运算符

关系运算符用来比较运算符左右两边的数据,返回的结果只会是boolean型的值。true/false

暂时无法在飞书文档外展示此内容

  1. 大于(>)
  2. 大于等于(>=)
  3. 小于(<)
  4. 小于等于(<=)
  5. 等于(==)
  6. 不等于(! =)
/*关系运算符*/
public class RelationOperator {
  public static void main(String[] args) {
    //==,    !=,    >,    <,    >=,    <=
    System.out.println(5==6);//false
    System.out.println(5==5);//true
    System.out.println(5!=6);//true
    System.out.println(5>6);//false
    System.out.println(5<6);//true
    System.out.println(5>=6);//false
    System.out.println(5<=6);//true

    //可以用于变量
    int a = 123;
    System.out.println(a >= 234);//false
    //用于表达式
    System.out.println( (15 + 20) != (3 * 20)  );
    //用于布尔型数据,布尔变量只能使用比较运算符的等于和不等于。
    System.out.println(true == false);
  }
}

7、逻辑运算

暂时无法在飞书文档外展示此内容

  • 与(&):两个都为true才是true,只要有一个是false就是false,可用于操作二进制位
  • 或(|):只要有一个为true就是true,都是false才是false,可用于操作二进制位
  • 非(!):针对一个变量,true会变成false, false会变成true;
  • 异或(^):两个相同为false,两个不相同为true;
  • 短路与(&&):和&类似;
  • 短路或(||):与|类似。

需要注意的是&和&&,以及|和||的区别。如果只是进行逻辑运算,它们也都是相同的,区别在于同时有其他操作的情况下,:

/*逻辑运算符*/
public class LogicOperator {
  public static void main(String[] args) {
    //与 &,  符号左右两边的表达式同时位true,整体返回true,其他情况都是false
    boolean result1 = (5 > 3) & (5==6);
    System.out.println(result1);

    //或 |  符号两边都为false时结果返回false,其他情况都返回true
    boolean result2 = (5 > 3) | (5==6);
    System.out.println(result2);

    //非 !
    System.out.println(true);
    System.out.println(!true);
    System.out.println(!((3+2) == 6));
    System.out.println("----------------&&-------||-------------");
    //与 &&,  符号左右两边的表达式同时位true,整体返回true,其他情况都是false
    boolean result3 = (5 > 3) && (5==6);
    System.out.println(result3);

    //或 ||  符号两边都为false时结果返回false,其他情况都返回true
    boolean result4 = (5 > 3) || (5==6);
    System.out.println(result4);

    //以下内容演示短路或与   或(|)的区别
    boolean a1 = true;
    int b1 = 0;
    boolean flag1 = a1 | b1++>0;
    System.out.println("b1 : " + b1);

    boolean a2 = true;
    int b2 = 0;
    boolean flag2 = a2 || b2++>0;
    System.out.println("b2 : " + b2);
    System.out.println('a' | 'b');//会计算两个字符的二进制
    System.out.println(true ||false);

    //以下内容演示短路与(&&) 与   与(&)的区别
    boolean a3 = false;
    int b3 = 0;
    boolean flag3 = a3 & b3++ > 0;
    System.out.println("b3 : " + b3);
    int b4 = 0;
    boolean flag4 = a3 && b4++ > 0;
    System.out.println("b4 : " + b4);

    System.out.println(15 & 8);//会与左右两个数的二进制
  }
}
  1. 按位与&:两位都为1才为1。
  2. 按位或|:只要有一位为1,就为1。
  3. 按位取反~:1变为0,0变为1。
  4. 按位异或^:相异为真,相同为假。
class BitDemo {
    public static void main(String[] args) {
        int bitmask = 0x1;
        int val = 0x0;
        // prints "2"
        System.out.println(val & bitmask);
        System.out.println(val | bitmask);
        System.out.println(val ^ bitmask);
        System.out.println(~bitmask);
        System.out.println(~val);
    }
}

8、位运算

Java 7之前不能单独表示一个位,但可以用byte表示8位,用十六进制写二进制常量。比如,0010表示成十六进制是0x2,110110表示成十六进制是0x36。位运算有移位运算和逻辑运算。移位有以下几种

  1. 左移:操作符为<<,向左移动,右边的低位补0,高位的就舍弃掉了,将二进制看作整数,左移1位就相当于乘以2。
  2. 无符号右移:操作符为>>>,向右移动,右边的舍弃掉,左边补0。
  3. 有符号右移:操作符为>>,向右移动,右边的舍弃掉,左边补什么取决于原来最高位是什么,原来是1就补1,原来是0就补0,将二进制看作整数,右移1位相当于除以2。
/*位运算*/
public class BitOperator {
  public static void main(String[] args) {
    int a = 8;
    System.out.println(a / 2);//4
    //右移
    System.out.println(a >> 1);//4
    //左移
    System.out.println(1 << 5);//32, 0000 0001 << 5      0010 0000 = 32;
    System.out.println(-10 >> 2);//计算过程如下。会先计算-10的二进制补码,再把补码右移两位
    //0000 0000 0000 0000 0000 0000 0000 1010
    //1111 1111 1111 1111 1111 1111 1111 0101   + 1
    //1111 1111 1111 1111 1111 1111 1111 0110
    //001111 1111 1111 1111 1111 1111 1111 01
    //在c语言有算术右移和逻辑右移的区别。java里只有逻辑右移
    // >>>
    System.out.println(8 >>> 2);
    //-1的移动
    System.out.println(-1 << 2);

    //按位取反 ~,把数值二进制位上的每一位取反
    byte bt = 10;
    System.out.println(~bt);
    //异或  ^,两个二进制位上只要相同结果就是0,不同为1
    byte bt1 = 7;
    byte bt2 = 8;
    System.out.println(bt1 ^ bt2);
    System.out.println(-1 ^ -1);// 1111 1111  ^  1111 1111
  }
}

9、三目运算符

img

/*三目运算符*/
public class TriadOperator {
  public static void main(String[] args) {
    //第一种写法
    int a = false ? 1 : 2;
    System.out.println(a);
    //第二种写法
    int b = (10 > 120) ? 1 : 2;
    System.out.println(b);
    //第三种写法,在三目运算符里调用方法
    int invokeMethod = (10 > 120) ? 1 : ma();
    System.out.println(invokeMethod);
    //第四种写法,在三目运算符里做计算
    int ath = (10 > 120) ? (3+2-5*0) : (8 ^ 7);
    System.out.println(ath);

    //第五种写法,在三目运算符里嵌三目运算符
    int nest = (10 < 120) ? (5 == 5) ? 1 : 2 : (8 ^ 7);
    System.out.println(nest);
    //第六种写法,在三目运算符里返回字符串
    String str = (10 > 120) ? (5 != 5) ? "hello" : "world" : "ikun";
    System.out.println(str);
  }

  public static int ma() {
    return 50;
  }
}

10、Scanner类

img

Scanner类属于java类库,java里的输入输出。

输入使用Scanner类。

输出System.out类。

在java语言里要使用类,需要首先创建该类的一个对象,创建对象使用关键字new

10.1 创建Scanner对象

10.2 调用Scanner类的next...方法

next()方法会阻塞当前程序的执行流程,等待用户输入数据并按下回车键

//告诉程序Scanner类放在java里的什么位置
import java.util.Scanner;
/*java里输入输出*/
public class ScannerDemo {
  public static void main(String[] args) {
    //输出, 使用类库中System类
    System.out.println("hello world");
    //输入,首先需要找到输入类在java类库中的位置
    //找到输入类之后需要创建该类的对象
    Scanner scanner = new Scanner(System.in);
    System.out.println("请输入:");
    //调用Scanner类的方法来读取内容
    String str = scanner.next();//该方法读取的值都会被当成字符串解析
    System.out.println("你输入的数据是 : " + str);

    //读取scanner输入的整数
    System.out.println("请输入整数:");
    String a = scanner.nextInt();//方法调用必须是方法名加括号
    System.out.println("a : " + a);
  }
}

10.3 hasNext(String w)方法

//告诉程序Scanner类放在java里的什么位置
import java.util.Scanner;
/*java里输入输出*/
public class ScannerDemo {
  public static void main(String[] args) {
    //输出, 使用类库中System类
    System.out.println("hello world");
    //输入,首先需要找到输入类在java类库中的位置
    //找到输入类之后需要创建该类的对象
    Scanner scanner = new Scanner(System.in);
    System.out.println("请输入:");
    //调用Scanner类的方法来读取内容
    //next()方法会阻塞当前程序的执行流程,等待用户输入数据并按下回车键
    String str = scanner.next();//该方法读取的值都会被当成字符串解析
    System.out.println("你输入的数据是 : " + str);

    //读取scanner输入的整数
    System.out.println("请输入整数:");
    int a = scanner.nextInt();//方法调用必须是方法名加括号
    System.out.println("a : " + a);
    System.out.println("请输入:");
    //只有子键盘输入的内容等于hasNext()括号内的内容时,结果才会是true。否则都是false
    System.out.println(scanner.hasNext("0"));
  }
}

作业

1、00101010 | 00010111语句的执行结果为

2、00101010 & 00010111语句的执行结果为

3、37.2%10的运算结果为 7.2

4、定义一个华氏度,转换成相应的摄氏度输出。(转换规则:摄氏度=5/9*(华氏度-32))

5、定义一个三位整数,分别输出其个位、十位和百位

6、定义一个四位整数,分别输出其个位、十位和百位、千位

7、完成打印输出Java所有基本数据类型及所占字节数,格式如效果图。

​ 类型 所占字节 取值范围 占多少位

​ byte 1字节 -2^7~2^7-1 8位

​ short 2字节 -2^15~2^15-1 16位

​ int 4字节 -2^31~2^31-1 32位

​ ...

8、从键盘输入三角形的底和高,并输出三角形的面积。

9、从控制台输入学员王浩3门课程(Java、SQL、Php)成绩,编写程序实现

​ (1)Java课和SQL课的分数之差

​ (2)3门课的平均分

10、定义两个变量int a = 10 int b = 20 ,交换两个变量的值

int a = 10, b = 20;
int c = a;
a = b;
b = c;

11、选做题

某个公司采用公用电话传递数据,数据是四位的整数,在传递过程中是加密的,

​ 加密规则如下:每位数字都加上3然后除以10的余数代替该数字,

​ 再将第一位和第四位交换,第二位和第三位交换。

​ 要求:键盘上输入四位号码,求加密后的号码为多少?

四、流程控制

程序有三种执行方式:顺序执行、分支选择执行、循环执行。

条件判断里,如果if或else后边只有一行代码,那么if...else后的大括号可以省略

在条件判断语句中,else语句是可选的,else不能脱离if独立存在。

1、条件判断

1.1、if

语法结构
if(条件表达式) {
    //程序
}
条件表达式返回的结果必须要是布尔类型的值。

/*if语句*/
import java.util.Scanner;
public class IfDemo {
  public static void main(String[] args) {
    //创建键盘输入对象
    System.out.println("请输入年龄:");
    Scanner sc = new Scanner(System.in);
    //读取键盘输入的年龄
    int age = sc.nextInt();
    //判断此人年龄是否大于18
    if(age > 18) {
      System.out.println("请进");
    }
    if(age < 18 && age > 0) {
      System.out.println("未成年");
    }
    System.out.println("程序结束");
  }
}

1.2、if...else

语法结构
if(条件表达式) {
    //程序
} else {
   //程序
}
注意:if和else两者里的代码一定会执行一个,不会出现都执行或者都不执行的情况

import java.util.Scanner;
public class IfDemo {
  public static void main(String[] args) {
    //创建键盘输入对象
    System.out.println("请输入年龄:");
    Scanner sc = new Scanner(System.in);
    //读取键盘输入的年龄
    int age = sc.nextInt();
    //判断此人年龄是否大于18
    if(age > 18)
      System.out.println("请进");
      //System.out.println("Test");
    else
      System.out.println("get out");
    System.out.println("程序结束");
  }
}

1.3、多重if

语法结构
if(条件判断) {
    //代码块
} else if(条件判断) {
    //代码块
} else if(条件判断) {
    //代码块
} else if(条件判断) {
    //代码块
} else {
    //代码块
}

import java.util.Scanner;
public class IfDemo {
  public static void main(String[] args) {
    //创建键盘输入对象

    Scanner sc = new Scanner(System.in);
    //需求,超市会员积分大于3000,可以享受6折优惠,大于2000小于3000享受8折,大于800小于2000可享受9折。
    //其他情况享受9.5折
    //定义一件商品的价格
    double price = 699.0;
    //定义一个变量记录用户的积分
    System.out.println("请输入你的积分:");
    int score = sc.nextInt();
    if(score > 3000) {
      System.out.println("6折后的价格是:" + (699.0 * 0.6));
    } else if(score > 2000 && score < 3000) {
      System.out.println("8折后的价格是:" + (699.0 * 0.8));
    } else if(score > 800 && score < 2000){
      System.out.println("9折后的价格是:" + (699.0 * 0.9));
    } else {
      System.out.println("9.5折后的价格是:" + (699.0 * 0.95));
    }
  }
}

随堂练习

根据键盘输入一个年龄,然后判断此人是属于人的哪一个阶段,人的阶段分为:婴儿[0~1岁)、幼儿[1~5岁)、儿童[5~12)、少年[12~18)、青年[18~36)、中年[36~60)、老年[60~125)。

System.out.println("请输入年龄:");
Scanner sc = new Scanner(System.in);
int age = sc.nextInt();
if (age ==0 )
    System.out.println("婴儿[0~1岁)");
else if (age >=1 && age < 5) 
    System.out.println("幼儿");
else if (age >=5 && age < 12) 
    System.out.println("儿童[5~12)");
else if (age >=12 && age < 18)
    System.out.println("少年[12~18)");
else if (age >=18 && age < 36) 
    System.out.println("青年[18~36)");
else if (age >=36 && age < 60)
    System.out.println("中年[36~60)");
else if (age >=60 && age < 125)
    System.out.println("老年[60~125)");
else
    System.out.println("请输入0—124的年龄");

1.4、嵌套if

语法结构
if(条件判断) {
    //属于外层if的代码块
    if(条件判断) {
        //代码块
    }
}

  public static void main(String[] args) {
    int a = 37;
    //a需要满足是偶数并且还要能够被6整数
    if( (a % 2) == 0) {
      System.out.println("a是一个偶数");
      //该数是否可以整除6
      if((a % 6) == 0) {
        System.out.println("满足条件");
      } else {
        System.out.println("不满足条件");
      }
    } else {
      System.out.println("该数不是偶数");
      //不是偶数就检查是否能够被7整除
      if((a % 7) == 0) {
        System.out.println("能被7整除");
      } else {
        System.out.println("不能被7整除");
      }
    }
    if(true) {
      //if内可以不写任何代码,但是这类写法没有实际意义。
    }
  }

1.5、switch

语法结构
switch(value) {//value的数据类型只能时整数类型(byte,short, char, int),String以及枚举(enum)
    case val1: break;
    case val2: break;
    case val3: break;
    default: System.out.println();
}

package Stage1;

import java.util.Random;
//中午吃啥?听系统的!
public class Lunch {
    public static void main(String[] args) {
        Random random = new Random();
        int number = random.nextInt(5);
        switch (number){
            case 0:
                System.out.println("中午饿着吧");
                break;
            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;
        }
    }
}



//G
System.out.println("请输入您的分数:");
Scanner scanner = new Scanner(System.in);
int number = scanner.nextInt();
switch (number / 10) {
    case 0:
    case 1:
    case 2:
    case 3:
    case 4:
    case 5:
        System.out.println("不及格,请家长");
        break;
    case 6:
        System.out.println("及格");
        break;
    case 7:
        System.out.println("良好");
        break;
    case 8:
        System.out.println("优秀");
        break;
    case 9:
    case 10:
        System.out.println("奖励小红花一枚");
    default:
        System.out.println("输入的成绩无效");
}

2、循环

2.1、for

img

注意:当循环体只有一条语句时,大括号可以省略不写。

语法结构
for(初始语句; 逻辑判断语句; 更新语句) {
    //循环体,也就是代码块。
}
初始语句: 就是一个简单的变量定义过程,如:int i = 0;
逻辑判断语句:和if圆括号内写的逻辑判断一模一样 如: i > 10;
更新语句:更新语句只要是指更新初始语句时定义的变量。

初始语句控制循环的开始,更新语句控制程序何时结束。

/*for循环*/
public class ForDemo {
  public static void main(String[] args) {
    /*
      for(初始语句; 逻辑判断语句; 更新语句) {
        循环体,也就是代码块。
      }
    */
    int a = 10;
    for(int i = 0; i < 20; i++) {
      System.out.println("i : " + i);
    }
  }
}

请使用for循环计算100的累加和。

int num = 0;//记录最后累加的和
for (int i = 1; i <= 100;i++){
    num += i;
}
System.out.println("1~100的累加和为:" + num);

请输出100以内的偶数,不要换行(print)。

System.out.print("1-100以内的所有偶数有:" );
for (int i=1;i<=100;i++){
    if (i%2==0){
        System.out.print( +i + " ");
    }
}

for (int i=2;i<101;i+=2){//i+=2 自增2
    System.out.print(i+" ");
}

嵌套for循环

语法结构
for(初始语句; 逻辑判断语句; 更新语句) {
    //循环体
    //代码
    for(初始语句; 逻辑判断语句; 更新语句) {
        //循环体
    }
    //代码
}

/*嵌套for循环*/
public class NestForLoop {
        public static void main(String[] args) {
                //需求:在终端输出 5 x 5的*号矩形
                for(int i = 0; i < 5; i++) {
                        //不换行输出
                        for(int j = 0; j < 5; j++) {
                                System.out.print(" * ");
                        }
                        System.out.println();//换行
                }
        }
}

使用for打印九九乘法表

//鲨鱼辣椒
int num = 0;
for (int i = 1; i <= 9;i++){
    for (int j = 1;j <= i;j++){
        System.out.print(i + "*" + j + "=" + num + "\t");
    }
    System.out.println();
}

for循环的多种写法

/*for循环的多种写法*/
public class ForLoopMultiType {
        public static void main(String[] args) {
                //第一种写法
                for(int i = 0; i < 5; i++) {
                        System.out.print("i = " + i + "  ");
                }
                
                System.out.println();
                //第二种
                int j = 0;
                for(; j < 5; j++) {
                        System.out.print("j = " + j + "  ");
                }
                System.out.println();
                //第三种写法
                int x = 0;
                for(;x < 5;) {
                        System.out.print("x = " + x + "  ");
                        x++;
                }
                System.out.println();
                //第四种写法
                for(;;) {
                        System.out.println("停不下来------");
                }
                /*for(;true;) {
                        System.out.println("停不下来------");
                }*/
        
        }
}
//渡星河
//这里是控制有多少行的,乘法表有九行,所以我们的判断条件就是小于10
for(int i = 1;i<10;i++){
    //这里是控制列,就是输出多少的,为什么要小于等于i,而不是10
    //我简单的说一下,因为我们是顺序输出的,乘法表是一个三角形,
    //我们的循环次数要控制数量,不可能写个10每次都来循环9次
    for(int j = 1;j<=i;j++){
        //这里这样写的原因是要让乘法表对齐,可以只要第二句
        //输出System.out.print(j+"×"+i+"="+(i*j)+"   ");
        if(j==3&&i==3||j==3&&i==4){
            System.out.print(" "+j+"×"+i+"="+(i*j)+"   ");
        }else{
            System.out.print(j+"×"+i+"="+(i*j)+"   ");
        }
    }
    //打印换行
    System.out.println("");
}

打印三角形

//Godv
//固定行数
for (int i=1;i<=5;i++){
    for (int q=1;q<=5-i;q++){
        System.out.print(" ");
    }
    for (int j=1;j<=i*2-1;j++){
        System.out.print("+");
    }
    System.out.println("");
}

//利用scanner输入行数
Scanner scanner = new Scanner(System.in);
System.out.println("输入需要打印等腰三角形的行数:");
int rows = scanner.nextInt();
System.out.println("等腰三角形效果如下:");

for (int i=1; i<=rows; i++) {
    //这里控制的是需要打印三角行的高度,也就是行数
    for (int j = 1; j <= rows - i; j++) {
        //这里控制的是空格,来显示出需要打印等腰三角的效果
        System.out.print(" ");
    }
    for (int j = 1; j <= 2 * i - 1; j++) {
        //控制每层的星星个数  这里的j <= 2 * i - 1;是根据上面控制行数那个i来乘再减1
        //如果是第一行就是 2 * 1 - 1
        //就是输出一颗星
        System.out.print("+");
    }
    System.out.println();
}
//渡星河
for (int i = 0;i<=7;i++){
    //打印左边空白
    for (int z =7;z>i;z--)System.out.print(" ");
    //打印半边三角形
    for (int zs = 0;zs<i;zs++)System.out.print("*");
    //打印右边
    for (int ys = 1;ys<i;ys++) System.out.print("*");
    //换行
    System.out.println();
}

打印菱形

//xhb
//打印等腰三角形
for (int i = 1;i <= 5;i++){  //控制三角形的层数,及高
    for (int j = 0;j <= 5-i;j++){   //打印出空白三角形
        System.out.print(" ");
    }
    for (int k = 1;k <=(2*i)-1;k++){   //打印出*等腰三角形
        System.out.print("*");
    }
    System.out.println();
}
//打印倒等腰三角形
for (int i = 1;i <= 5;i++){  //控制三角形的层数,及高
    for (int j = 0;j <= i;j++){   //打印出空白三角形
        System.out.print(" ");
    }
    for (int k = 1;k <=(2*(5-i))-1;k++){   //打印出*等腰三角形

        System.out.print("*");
    }
    System.out.println();
}




//Godv

//两个等腰三角形来拼接
//这是上面一个
for (int i=1;i<=5;i++){
    for (int t=1;t<=5-i;t++){
        System.out.print(" ");
    }
    for (int j=1;j<=i*2-1;j++){
        System.out.print("+");

    }
    System.out.println("");
}
//这是下面一个
for (int i=1;i<=5;i++){
    for (int t=1;t<=i+1-1;t++){
        System.out.print(" ");
    }
    for (int j=7;j>=i*2-1;j--){
        System.out.print("+");

    }
    System.out.println("");
}

2.2、while

//for循环的第三种写法
                int x = 0;
                for(;x < 5;) {
                        System.out.print("x = " + x + "  ");
                        x++;
                }

while循环的语法结构

while(逻辑表达式) {
    //循环体
}
/*while循环*/
public class WhileLoop {
        public static void main(String[] args) {
                //需求:使用while循环计算100以内的偶数累加和
                //定义变量接收累加后的结果
                int sum = 0;
                //初始表达式
                int even = 2;
                while(even < 101) {//逻辑判断语句
                        sum += even;
                        //更新语句
                        even += 2;
                }
                System.out.println("结果 : " + sum);
        }
}

2.3、do while

语法结构
do {
    //循环体
} while(条件判断语句);

/*do...while循环*/
public class DoWhileLoop {
        public static void main(String[] args) {
                //需求:使用do...while循环计算100以内的偶数累加和
                //记录累加的结果
                int sum = 0;
                //初始语句
                int count  = 1;
                do {
                        //打印偶数
                        if( (count % 2) == 0 )
                                sum += count;
                        count++;//更新语句
                } while(count < 101);//条件判断
                System.out.println("sum = " + sum);
        }
}

循环互相嵌套

/*九九乘法表*/
public class NestForWhileDoWhile {
        public static void main(String[] args) {
                for(int i = 1; i <= 9; i++) {
                        int x = 1;
                        while(x <= i) {
                                System.out.print("-----------");
                                if(i > 1)System.out.print("--");
                                x++;
                        }
                        System.out.println();
                        int j = 1;
                        do {
                                System.out.print(String.format("|%s x %s = %s|  ", j, i, (j*i)));
                                j++;
                        } while(j <= i);
                        System.out.println();
                }
        }
}

2.5、死循环

通常用在实现服务器

public class NestForWhileDoWhile {
        public static void main(String[] args) {
                /*for(;;) {
                        System.out.println("炸裂");
                }*/
                /*while(true) {
                        System.out.println("炸裂");
                }*/
                
                do {
                        System.out.print("doger");
                } while(true);
        }
}

2.6、关键字

break

强行终止当层循环

img

for(int i = 0; i < 100; i++) {                
                        //当i等于50时使用break终止循环
                        if(i == 50)break;
                        System.out.println(" i : " + i);
                        
                }

break label

跳转到label标识的位置

img

                one:
                for(int i = 0; i < 100; i++) {
                        two:
                        for (int j = 0; j < 50; j++) {
                                if(j == 20)break two;
                                System.out.println("j = " + j);
                        }
                }

continue

img

结束本次循环继续执行下一次循环

for(int i = 0; i < 100; i++) {                
                        //当i等于50时使用break终止循环
                        if((i%2) == 0)continue;
                        System.out.println(" i : " + i);
                        
                }

continue label

img

作业

1、输出100以内,所有不是3的倍数的数

2、计算1至50中是7的倍数的数值之和

3、循环输入数字1-7 ,输出对应的星期几。(输入0时循环结束)

4、打印空心矩形

5、计算2000年1月1日到2020年1月1日相距多少天(能被4整除但不能被100整除,能被400整除的为闰年。)

6、有数列:9,99,999,9999...,编程计算前10项值的和。

7、输出得斐波那契数列前20项的值并输出。

8、需求说明:

会员购物时,根据输入积分的不同享受不同的折扣

计算会员购物时获得的折扣输出实付金额

  • 会员积分x 折扣
  • x < 2000 9折

2000 ≤ x < 4000 8折

4000 ≤ x < 8000 7折

x ≥ 8000 6折

9、用户输入两个数a和b,如果a能被b整除或者a加b大于100,则输出a,否则输出b。

10、定义一个数,求得其二进制(正数)

选做题

1、打印指定月份的日历信息(实现从键盘输入1900年之后的任意的某年、某月,输出该月的日历)

如下:2022年6月

星期一星期二星期三星期四星期五星期六星期日
12345
6789101112
13141516171819
20212223242526
27282930

2、使用循环输出杨辉三角函数

int sum = 0;
for(int i = 1; i <= 5;i++) {
    sum=1;
    for(int k = 20-2*i;k > 0;k--)
        System.out.print(" ");

    for(int j = 1;j <= i;j++) {
        if(j > 1)
            sum = sum*(i-j+1) / (j-1);   //公式
        System.out.print(String.format("%5d",sum));
    }
    System.out.println();
}

五、方法

方法的定义

执行指定功能的代码块。

简单方法定义

暂时无法在飞书文档外展示此内容

返回值类型 方法名() {
    //方法体
}
void showMultiTable() {}

复杂方法定义

  1. 无返回值方法

暂时无法在飞书文档外展示此内容

返回值类型 方法名(形参列表 形参名)  {注意:形参都属于局部变量
    //方法体
}

调用方法的时候在括号内写的参数叫做实际参数,简称实参
void calculateSum(int max) {}
//定义有返回值类型的方法
//需求:返回计算后的结果,也就是方法内部计算出来结果后要返回到调用的地方
//有返回值方法的定义
int calculateSumResult() {}
  1. 有返回值方法

暂时无法在飞书文档外展示此内容

形参和实参

  1. 形参

定义方法时写到方法名后括号内的参数称为形式参数,简称形参。形参可以有多个,可以是各种数据类型。具体可参查看下方“方法调用”模块的代码示例

  1. 实参

调用方法时填入括号内的参数叫做实参,实参必须要和被调方法的形参数量、形参数据类型、形参顺序保持一致。具体可参查看下方“方法调用”模块的代码示例

方法的分类

  1. 关键字static

img

  1. 静态方法

被static修饰的方法和属性以及代码块。被修饰的方法和属性(变量)叫做静态方法和静态属性

  1. 成员方法

没有被static修饰的方法和属性叫做成员方法和成员属性(变量),成员方法和成员属性必须要通过创建对象去访问。

方法调用

调用方法的两种方式

一:创建类的对象来调用, 通过对象的 变量名或叫做引用.方法名() 的形式调用。以下代码示例中,Test类里的SimpleMethod sm = new SimpleMethod();语句就是通过关键字new创建对象。而此处的sm就是变量名或叫做引用。

//SimpleMethod.java
public class SimpleMethod {
    //无返回值方法
    void calculateSum(int max) {//业务代码}
    //有返回值方法
    int calculateSumResult() {
        //业务代码
        return 100;
    }
    //有参方法
    void calculateSum(int min, int max) {//业务代码}
}

//Test.java
public class Test {
    public static void main(String[] args) {
        SimpleMethod sm = new SimpleMethod();//创建对象
        /*此处的50就是调用calculateSum(int max)方法的实参,而max叫做形参。
        实参要和形参的数据类型必须保持一致。*/
        sm.calculateSum(50);//调用方法
        //调用有返回值的方法
        int result = sm.calculateSumResult();//可以接收返回值
        sm.calculateSumResult();//也可以选择不接收返回值
        //sm.calculateSum(10, 50);该调用括号内的实参顺序要和方法
        //calculateSum(int min, int max)内的新参顺序保持一致,
        //也就是实参10会传给min形参, 实参50会被传给max形参
        //注意:如果方法定了为有参方法,那就必须要在调用时带上实参,否则程序会报错。
        sm.calculateSum(10, 50);
    }
}

调用有返回值方法时注意事项:

  1. 方法的返回值使用 return关键字,如:return value 此处value可以是常量、变量、表达式、方法
  2. 方法的返回值也就是return关键字后边跟的value值数据类型要和方法定义中的返回值类型一致或者数据表示返回大于返回的值
  3. 调用方法代码里可以不用接收返回的值,也可以选择接收。
  4. 当需要接收方法的返回值时,接收方定义的变量必须要和方法的返回值类型保持一致
//sm.calculateSum(10, 50);该调用括号内的实参顺序要和方法calculateSum(int min, int max)
//内的新参顺序保持一致,也就是10会传给min形参, 50会被传给max形参
注意:如果方法需要调用方提供参数,那就必须要在调用是填写上实参,否则程序会报错。
SimpleMethod sm = new SimpleMethod();
sm.calculateSum(10, 50); 
void calculateSum(int min, int max) {}

二:使用 类名.方法名()` ` 的形式调用

要使用这种方式调用方法的前提:方法必须要被static关键字修饰

暂时无法在飞书文档外展示此内容

该种调用方法的形式,在形参和实参的处理上和上一种调用方式一样。

//SimpMethod.java
public class SimpleMethod {
    //打印九九乘法表
    static void showMultiTable() {
        System.out.println("打印九九乘法表");
        for (int i = 1; i <= 9;i++){
            for (int j = 1;j <= i;j++){
                System.out.print(String.format("%d*%2d=%2d  ", j, i, (j*i)));
            }
            System.out.println();
        }
    }
}
//Test.java
public class Test {
    public static void main(String[] args) {
        SimpleMethod.showMultiTable();
    }
}

静态方法和成员方法调用代码示例

//MethodInvoke.java
public class MethodInvoke { 
    void method01() {}
    int method02(String s, int  a) {return 10;}
    static void method03() {}
    static int method04(int a) {return 3;}
    static String method05(String s, double d){return "ok";}
}
//Test.java    测试类
public class Test {
    public static void main(String[] args) {
        //调用静态方法
        MethodInvoke.method03();//无参方法调用
        int result = MethodInvoke.method04(10);//10叫做实参,result是用来接收返回值的变量。
        //该方法有两个实参,第一个数据类型是字符串,第二个是整型
        //response是用来接收返回值的变量。
        String response = MethodInvoke.method05("good", 101.0);

        //调用成员方法
        MethodInvoke mi = new MethodInvoke();//创建对象, mi叫引用
        mi.method01();//通过引用调用方法
        int res = mi.method02("good", 101);
    }
}

为什么静态方法可以使用类名访问,而成员方法却需要创建对象?

暂时无法在飞书文档外展示此内容

上图中有对象的引用mi1mi2,两个引用代表两个对象,这两个对象都有独立的内存空间,并且都有属于自己的方法method01method02

main方法和使用类在同一个文件时的方法调用示例

public class MethodDemo01 {
    static int a = 0;
    int b = 0;

    //定义成员方法
    void m1(){
        System.out.println("方法m()");
    }
    void m2(char ch) {
        System.out.println("方法m2()");
    }
    String m3(int a, String s) {
        System.out.println("方法m3()");
       return "ok";
    }
    //定义静态方法
    static void m4() {
        System.out.println("static 方法m4()");
    }
    static void m5(int a) {
        System.out.println("static 方法m5()");
    }
    static String m6(String s, int a) {
        System.out.println("static 方法m6()");
        return "";
    }
    //测试方法
    public static void main(String[] args) {
        //调用成员方法,首先创建对象
        MethodDemo01 methodDemo01 = new MethodDemo01();
        //调用方法m1  引用名.方法([参数列表])
        methodDemo01.m1();
        methodDemo01.m2('a');
        methodDemo01.m3(12, "你好");

        //调用静态方法1
        m4();
        m5(10);
        int c = 200;
        m6("hessfsd", c);

        //调用静态方法2
        MethodDemo01.m4();
        MethodDemo01.m5(c);
        methodDemo01.m6("uuuu", c);
    }
}

修饰符public

java里完整的方法定义

Java的方法组成如下:
修饰符 返回值类型 方法名(参数类型 参数名...){  
    //方法体
 }

img

示例

public class MethodDemo01 {
    public static int a = 0;
    public int b = 0;

    //定义成员方法
    public void m1(){
        System.out.println("方法m()");
    }
    public void m2(char ch) {
        System.out.println("方法m2()");
    }
    public String m3(int a, String s) {
        System.out.println("方法m3()");
       return "ok";
    }
    //定义静态方法
    public static void m4() {
        System.out.println("static 方法m4()");
    }
    public static void m5(int a) {
        System.out.println("static 方法m5()");
    }
    public static String m6(String s, int a) {
        System.out.println("static 方法m6()");
        return "";
    }
    //测试方法
    public static void main(String[] args) {
        //调用成员方法,首先创建对象
        MethodDemo01 methodDemo01 = new MethodDemo01();
        //调用方法m1  引用名.方法([参数列表])
        methodDemo01.m1();
        methodDemo01.m2('a');
        methodDemo01.m3(12, "你好");

        //调用静态方法1
        m4();
        m5(10);
        int c = 200;
        m6("hessfsd", c);

        //调用静态方法2
        MethodDemo01.m4();
        MethodDemo01.m5(c);
        methodDemo01.m6("uuuu", c);
    }
}

return关键字

  1. 从当前的方法中退出,返回到该调用方法的语句处,继续执行。
  2. 返回一个值给调用该方法的语句,返回值数据类型必须与方法的声明中返回值的类型一致,可以使用强制类型转换来使数据类型一致。

return语句主要有两个用途: 一方面用来表示一个方法返回的值,另一方指它导致该方法退出,并返回那个值。根据方法的定义,每一个方法都有返回类型,该类型可以是基本类型,也可以是引用类型,同时每个方法都必须有个结束标志,因此,return起到了这个作用。在返回类型为void的方法里面,有个隐含的return语句,因此,在void方法里面可以省略不写。 2、Java中,return,break,continue以及goto的区别。但是goto不常用,感兴趣的可以了解。 return语句:是指结束该方法,继续执行方法后的语句。 break语句:是指在循环中直接退出循环语句(for,while,do-while,foreach),break之后的循环体里面的语句也执行。跳出switch分支。 continue语句:是指在循环中中断该次循环语句(for,while,do-while,foreach),本次循环体中的continue之后语句不执行,直接跳到下次循环。

img

注意:

  1. return可以用在有返回值的方法中
  2. return也可以用在没有返回值的方法中:
  3. 在return的后面不能直接写任何代码,因为不可能执行到此处的代码。
  4. 没有返回值的方法可以写return,有返回值的方法必须要写return 返回值;

方法的重载

方法名相同,方法参数的数据类型,参数个数以及参数顺序不同称为方法的重载。

重载的作用是为了功能类似的方法名使用同一个,便于记忆,因此使用起来更方便。

暂时无法在飞书文档外展示此内容

/**
 * 演示方法重载,方法名相同,参数个数,参数的数据类型以及参数顺序不同就是方法的重载
 */
public class OverLoad {
    public static void main(String[] args) {
        //方法的调用
        show();
        show((short) 45);
        show((short) 75, 89);
        show(105, (short)879);
    }

    public static void show() {
        System.out.println("no parameter-----");
    }
    public static void show(short s) {
        System.out.println("a parameter-------");
    }
    //下边两个方法演示:参数顺序不同就可以使用相同的方法名
    public static void show(short s, int a) {
        System.out.println("show(short s, int a)");
    }
    public static void show(int a, short s) {
        System.out.println("show(int a, short s)");
    }
}

递归

定义

递归(英语:Recursion),在数学计算机科学中,是指在函数的定义中使用函数自身的方法。简单理解就是方法自己调用自己。

img

练习:使用递归输出斐波拉契前8项

//    调用fblqRecursion(1,1,8);
    public static int fblqRecursion(int a,int b,int n){//斐波拉契前8项
        n = n - 1;
        if(a==1 && b==1){
            System.out.print("\t"+ a + "\t"+ b);
            return fblqRecursion(b,a+b, n);
        }
        if(n > 0){//第八次条件
            System.out.print("\t"+ (a + b));
            return fblqRecursion(b,a+b, n);
        }
        else
            return 0;
    }
static int count = 2;
static int povit = 0;
public static void main(String[] args) {
    fibonacci(8);
}


public static int fibonacci(int n) {
    if (n < 3) {
        //因为在递归调用的过程中会有很多次n < 3,为了控制只输出两次,
        //所以使用一个计数变量
        if (count > 0)
            System.out.print(1 +  "  ");
        count--;
        return 1;
    }
    int a = fibonacci(n-1) + fibonacci(n-2);
    //a的值可能会出现中间结果,但是我们不希望中间结果也输出,所以使用if判断
    //如果输出的中间结果小于下一个要输出的结果,那就不输出中间结果
    if (povit < a) {
        System.out.print(a + "  ");
        povit = a;
    }
    return a;
}
public static void main(String[] args) {
    for (int i = 1;i <= 8;i++){
        System.out.print(Fibonacci(i) + " ");
    }
}
/*
* 使用递归输出斐波拉契前8项
* */
public static int Fibonacci(int n){
    if (n <= 2){
        return 1;
    }else {
        return Fibonacci(n - 1) + Fibonacci(n - 2);
    }
}

汉诺塔

/**
 * 
 * @param n 表示柱子上的盘子数
 * @param a  表示第一根柱子
 * @param b  表示第二根柱子
 * @param c  表示第三根柱子
 */
public static void hanoi(int n, char a, char b, char c) {
    if(n == 1) {
        System.out.println("把第"+ n + "个碟片从 " + a + " 移动到 " + c);
        return;
    }
    hanoi(n-1, a, c, b);
    System.out.println("把第"+ n + "个碟片从 " + a + " 移动到 " + c);
    hanoi(n-1, b, a, c);
}

//调用
hanoi(4, 'A', 'B', 'C');

方法的优点

  1. 使主程序更简洁,更易于理解。
  2. 代码可以重复使用
  3. 易于开发、调试、测试
  4. 更方便团队合作

方法使用注意事项

  1. 方法必须定义在类里
  2. 方法不能嵌套
  3. 在return的后面不能直接写任何代码,因为不可能执行的到
  4. 没有返回值的方法可以写return,有返回值的方法必须要写return 返回值;
  5. 方法在某些语言中被称为过程、函数。processe, function, method

String类

该类是java自带的,表示字符串。

String属于引用类型,要使用引用类型,首先要创建对象。

public class StringDemo {
    public static void main(String[] args) {
        //第一种定义字符串的形式
        String s = "abc";
        //第二种定义字符串的形式
        String str = new String("abc");
        String s1 = "abcsaf";
        //字符串比较推荐使用equals方法, 此方法才会真正比较字符串的内容。==是比较地址。
        System.out.println(s.equals(s1));
        System.out.println(s.equals(str));
        System.out.println(s == str);
        System.out.println(s == s1);//true的原因是地址一样
        //获取字符串长度
        System.out.println(s1.length());
        //字符串替换
        System.out.println(s1);
        System.out.println(s1.replace("csa", " "));
        //把小写字符串转成大写
        System.out.println(s1.toUpperCase());
        //把大写转小写
        System.out.println(s1.toUpperCase().toLowerCase());
        //查看字符串中是否包含自定子串
        System.out.println(s1.contains("sf"));
    }
}

作业

  1. 写一个方法,打印九九乘法表
  2. 写一个方法,输入三角形的三条边,输出是否能构成三角形
//曾昭洋
triangle(3,4,2);
private static void triangle(int a,int b,int c) {
    if (a + b >c && a - b < c &&  b - a < c ){//两边之和 > 第三边,两边之差 < 第三边
        System.out.println("能构成三角形");
    }else System.out.println("不能构成三角形");
}
  1. 写一个方法,传入用户名和密码,返回是否登录成功(默认正确的用户名密码为 admin和123456)
  2. 写一个方法,传入两个数字,并传入一个类型(例如:传入+号就做加法运算,*号就是乘法运算)。返回两个数计算的结果
  3. 定义一个方法,计算商品的价格(参数1:数量 参数2:苹果(3.5元) 香蕉 (4元) 橘子(5元))
  4. 定义一个方法,传入一个数字,判断是否是质数,返回boolean
  5. 定义一个方法,求1000以内,最大的水仙花数,并返回
//曾昭洋
System.out.println("最大水仙花数为:" + narcissus());

private static int narcissus() {
    int max = 0;
    for (int i = 100; i < 1000; i++) {
        double a = Math.pow((i % 10), 3);//求出 个位数 3次方
        double b = Math.pow(((i / 10) % 10), 3);
        double c = Math.pow((i / 100), 3);
        if (a + b + c == i){
            if (max < i){//如果当前 水仙花数>之前最大的水仙花数
                max = i;//那么就代替掉之前最大的数
            }
        }
    }
    return max;
}
  1. 定义一个方法,求100以内,所有的质数并输出
  2. 定义一个方法,传入三个数,输出最大的那个数,最小的那个数
//
public void nine(int a ,int b ,int c ){
    System.out.printf("最大的数是%d%n最小的数是%d",(a>b&&a>c)?a:(b>a&&b>c)?b:c,(a<b&&a<c)?a:(b<a&&b<c)?b:c);
}
  1. 定义一个方法,接收一个参数,使用递归输出该参数每一位上的值
  2. 验证哥德巴赫猜想:任何一个大于6的偶数,都能分解成两个质数的和,要求输入一个整数,输出这个数被被分解成哪两个质数的和。

    1. 如:14
    2. 14 = 3 + 11
    3. 14 = 7 + 7

六、数组

为什么要学习数组?

一维数组

在计算机科学中,数组数据结构(英语:array data structure),简称数组(英语:Array),是由相同类型的元素(element)的集合所组成的数据结构,分配一块连续的内存来存储。利用元素的索引(index)可以计算出该元素对应的存储地址。最简单的数据结构类型是一维数组。例如,索引为0到9的32位整数数组,可作为在存储器地址2000,2004,2008,...2036中,存储10个变量,因此索引为i的元素即在存储器中的2000+4×i地址。数组第一个元素的存储器地址称为第一地址或基础地址。

img

静态初始化

语法结构:数据类型[] 数组名 = {value1, value2, value3, ......, valuen};

数据类型可以是八种基本数据类型和引用类型

数组名即变量名,可以自定义,遵循变量命名规则:不能以数字开头,取名做到见名之意,多个单词使用驼峰标识

暂时无法在飞书文档外展示此内容

/**
 * 数据类型[] 数组名 = {value1, value2, value3, ..., valuen};
 */
public class StaticInit {
    public static void main(String[] args) {
        //定义字符串数组
        String[] students = {"Bobs", "Steven", "Kobe"};
        //获取数组内的元素使用语法 :  数组名[下标]
        String studentName = students[0];
        System.out.println(studentName);
        //获取数组的长度使用语法 : 数组名.length
        int len = students.length;
        System.out.println("数组长度 : " + len);
        System.out.println(students[2]);
        //更新和插入数组内的元素语法 : 数组名[下标] = 更新或插入后的新值
        students[2] = "Curry";
        System.out.println(students[2]);
    }
}

语法结构:数据类型[] 数组名 = new 数据类型[数组长度];

数据类型可以是八种基本数据类型和引用类型

数组名即变量名,可以自定义,遵循变量命名规则:不能以数字开头,取名做到见名之意,多个单词使用驼峰标识

new是java的关键字,必须保持不变。

数组长度即是分配数组后,数组分成多少个单元格。

动态初始化

/**
 * 动态初始化数组 数据类型[] 数组名 = new 数据类型[数组长度]{}
 */
public class DynamicInit {
    public static void main(String[] args) {
        //定义一个字符串数组
        String[] students = new String[3];
        //定义一个int型数组
        int[] ages = new int[3];
        //打印输出数组默认值
        System.out.println(students[0]);
        System.out.println(students[1]);
        System.out.println(students[2]);
        //获取数组长度
        System.out.println(students.length);
        //获取年龄数组的长度
        System.out.println(ages.length);
        //给students数组添加元素
        System.out.println("students数组第一个元素" + students[0]);
        students[0] = "Bobs";//在数组下标为0的位置添加值 "Bobs", 即数组的第一个元素
        students[2] = "Kobe";//在数组下标为2的位置添加值 "Bobs",即数组的第三个元素
        System.out.println("students数组第一个元素" + students[0]);
        //修改第一个元素为Jackson
        students[0] = "Jackson";
        System.out.println("students数组第一个元素" + students[0]);
        //获取超过数组下标范围的元素
        //System.out.println(students[students.length]);数组下标越界
        //给数组内的元素在运行时动态赋值
        String name = "国伟";
        students[1] = name;
        System.out.println(students[1]);
    }
}

练习

代码完成静态和动态初始化创建数组

使用动态初始化定义各种数据类型的数组,然后打印查看数组的默认值。

遍历

/**
 * 遍历数组
 */
public class TraverseArray {

    public static void main(String[] args) {
        //静态初始化定义字符串数组
        String[] students = {"Bobs", "Steven", "Kobe"};
        //访问数组元素
        for (int i = 0; i < students.length; i++) {
            //输出每一个下标位置的元素
            System.out.print(String.format("数组的第 %d 个元素是 %s \n", (i+1),students[i]));
        }
    }
}

练习

1、从键盘录入五个名字到数组中,遍历数组输出这五个名字

public static void main(String[] args) {
    String name[]=new String[5];
    Scanner scanner = new Scanner(System.in);
    System.out.println("”...”可结束输入!");
    for (int i=0;i< name.length;i++) {
        System.out.print("输入名字"+(i+1)+":");
        String namennn = scanner.next();
        if (namennn.equals("...")){
            break;
        }else {
            name[i] = namennn;
        }
    }
    bianli(name);
}

public static void traverse(String a[]){
    for (int i=0;i< a.length;i++) {
        System.out.print(String.format("数组的第 %d 个元素是 %s \n", (i+1),a[i]));
    }
}
//zyy

2、给定一个数组,求数组的最大值

int[] arr = {1, 26, 7, 12, 30, 8};
public int max() {
    int max = 0;
    for( int i = 0; i < arr.length; i++)
        if(arr[i] > max) max = arr[i];       //曾昭洋:max = Math.max(arr[i],max);
    return max;                              //System.out.println(String.format("最大的数是:%d  ",max));
}

3、给定一个数组,求数组的最小值

int[] arr = {1, 26, 7, 12, 30, 8};
public int min() {
    int min = 0;
    for( int i = 0; i < arr.length; i++)
        if(arr[i] < min) min = arr[i];
    return min;
}

4、给定一个数组,求数组的平均值

int[] arr = {1, 26, 7, 12, 30, 8};
public int min() {
    int sum = 0
    for( int i = 0; i < arr.length; i++)
        sum += i;
    return sum/arr.length;
}

5、给定一个数组,传入一个数字,如果在数组中存在这个数字,返回这个数字在数组中的下标,否则返回-1

int[] arr = {1, 26, 7, 12, 30, 8};
public int find(int x) {
    int sum = 0
    for( int i = 0; i < arr.length; i++)
        if(x == arr[i]) return i;
    return -1;
}

6、思考题:在第一题的基础上。但是要保证五个名字不重复

String[] names = new String[5];
int count = 0;
public void add(String name) {
    if(count >= arr.length)return;
    for( int i = 0; i < count; i++)
        if(names[i].equals(name)) System.out.println("名字已经存在");
    names[count++];
}

拷贝数组

/**
 * 拷贝数组
 */
public class CopyArray {
    public static void main(String[] args) {
        //静态初始化定义字符串数组
        String[] students = {"Bobs", "Steven", "Kobe"};
        System.out.println("拷贝前的数组");
        traverse(students);
        students = copyArray(students, 1,  1);
        System.out.println("拷贝后的数组");
        traverse(students);
    }
    /**
     * 拷贝数组
     * @param oldArr  数组引用
     * @param index  需要拷贝数组的起始下标
     * @param count  需要拷贝的元素个数
     */
    public static String[] copyArray(String[] oldArr, int index, int count) {
        if (index < 0 || index >= oldArr.length || count < 1) {
            System.out.println("无效参数,拷贝失败!");
            return oldArr;
        }
        //新数组长度
        int newArrLen = index + count;
        if (newArrLen > oldArr.length) count = oldArr.length - index;
        //根据传入的count确定新建数组大小
        String[] newArr = new String[count];
        //第一种
        for (int i = 0; i < newArr.length; i++) {
            newArr[i] = oldArr[index++];
        }
        return newArr;
    }

    public static void traverse(String[] arr) {
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + "  ");
        }
        System.out.println();
    }
}

Arrays和System类拷贝数组

import java.util.Arrays;
/**
 * System和Arrays拷贝数组
 */
public class JDKArrayCopy {
    public static void main(String[] args) {
        //静态初始化定义字符串数组
        String[] students = {"Bobs", "Steven", "Kobe"};
        //使用System的拷贝方法
        /*show(students);
        System.arraycopy(students, 0, students, 1, 2);
        show(students);*/
        String[] students02 = new String[students.length];
        show(students02);
        System.arraycopy(students, 0, students02, 0, students.length);
        show(students02);

        System.out.println("----------------------------------------");
        //使用Arrays里的拷贝方法
        String[] results = Arrays.copyOf(students, students.length);
        show(results);
    }
    //遍历查看数组元素
    public static void show(String[] stus) {
        for (int  i = 0; i < stus.length; i++)
            System.out.print(stus[i] + "  ");
        System.out.println();
    }
}

将数组传递给方法

/**
 * 演示值传递和引用传递 
 */
public class ArrayPassParameters {
    public static void main(String[] args) {
        //定义变量
        int a = 10;
        //调用方法
        updataIntVira(a);
        System.out.println("a的值 : " + a);
        //静态初始化定义字符串数组
        String[] students = {"Bobs", "Steven", "Kobe"};
        updateArrayValue(students);
        System.out.println(students[0]);
    }

    /**
     * 值传递示例
     * @param a
     */
    public static void updataIntVira(int a) {
        a = 101;
    }

    /**
     * 引用传递示例
     * @param stus
     */
    public static void updateArrayValue(String[] stus ) {
        stus[0] = "Error";
    }
}

扩容

import java.util.Arrays;

/**
 * 遍历数组
 */
public class GrowArray {
    public static void main(String[] args) {
        //静态初始化定义字符串数组
        String[] students = {"Bobs", "Steven", "Kobe"};
        show(students);
        //使用JDK自带的Arrays完成扩容,数组大小扩容为原来的两倍
        students = Arrays.copyOf(students, students.length << 1);
        //扩容后继续添加元素
        students[3] = "James";
        show(students);//调用遍历方法
    }
    //遍历数组
    public static void show(String[] arr) {
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + "  ");
        }
        System.out.println();
    }
}

JDK ArrayList

public class MyArray {
    //全局数组
    static String[] names = null;
    //定义变量记录数组的有效元素个数
    static int size = 0;
    public static void main(String[] args) {
        //初始化数组
        initArray(3);
        //添加数据
        add("张一");
        add("张二");
        add("张三");
        add("张四");
        add("张五");
        add("张六");
        add("张七");
        add("张八");
        show();
        System.out.println(names.length);
        delete("张七");
        delete("张八");
        delete("张六");
        show();
        System.out.println(names.length);
        System.out.println("size : " + size);
    }0
    public static void initArray(int capacity) {
        names = new String[capacity];
    }

    //扩容和缩容
    public static void updateArraySize(int len) {
        //创建新数组,容量是旧数组的两倍
        String[] newArr = new String[len];
        //拷贝元素
        for (int i = 0; i < size; i++) {
            newArr[i] = names[i];
        }
        //改变引用
        names = newArr;
        newArr = null;
    }
    //添加名字
    public static void add(String value) {
        //当size等于数组长度时进行扩容
        if (size == names.length) {
        //names.length << 1 表示长度除以二
           updateArraySize(names.length << 1);
        }
        names[size++] = value;
    }

    //删除
    public static void delete(String value) {
        int index = find(value);
        if (index == -1) {
            System.out.println(value + " 不存在系统中");
            return;
        }
        //移动下标index之后的所有有效元素
        for (int i = index + 1; i < size; i++) {
            names[i-1] = names[i];
        }
        size--;
        //如果空闲空间达到数组的一半,就进行缩容
        boolean free = ((names.length / size) >= 2) ? true : false;
        if (free) {
            //就进行缩容
            //新建数组
            updateArraySize(size);
        }
    }
    //查找指定元素并返回下标
    public static int find(String value) {
        //循环数组比较元素
        for (int i = 0; i < size; i++) {
            if (names[i].equals(value)) {
                return i;
            }
        }
        return -1;
    }

    //遍历数组
    public static void show() {
       for (String name : names)
           System.out.print(name + "  ");
        System.out.println();
    }
}

练习

不要使用系统自带类完成数组扩容。

public static void main(String[] args) {
    String arr[]={"小芳","小华","小朱","小渣渣"};
    String newArray[]=Expansion(arr);
    newArray[4]="小美";
    traverse(newArray);
}
public static void traverse(String a[]){//遍历
    for (int i=0;i< a.length;i++) {
        System.out.print(a[i]+" ");
    }
}
public static String[] Expansion(String a[]){//扩容
    String newArr[]=new String[2*a.length];
    for (int i=0;i<a.length;i++){
        newArr[i]=a[i];
    }
    return newArr;
}

使用System.arraycopy()完成拷贝。

//曾昭洋
String[] arr ={"张三","李四","王五"};
String[] newArr = new String[arr.length << 1];
show(sysArr(arr,newArr));

private static String[] sysArr(String[] arr,String[] newArr) {
    // 原数组,原数组起始位置,目标数组,目标数组起始位置,复制的数量
    System.arraycopy(arr,0,newArr,0,arr.length);
    return newArr;
}

public static void show(String[] arr){
    for (int i = 0; i < arr.length; i++) System.out.println(arr[i]);
}

方法形参可变长列表

语法格式:public void 方法名(数据类型... 参数名) {//方法体}

这里的参数名其实是一个数组的引用。它会根据传入的参数个数动态生成一个数组来存放接收到的参数。

/**
 * 可变长方法参数列表
 */
public class DynamicMethodParameter {
    public static void main(String[] args) {
        int[] b = method01(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6);
        System.out.println(b[2]);
        method01(1,2);
        method01();
    }

    public static int[] method01(int... a) {
        System.out.println(a.length);
        return a;
    }
}

foreach循环

jdk1.5之后的新语法,用来代替for遍历数组或集合,称为增强for循环。只能逐个访问数组的元素,不能操作数组的下标。

语法格式:

for(数据类型 变量名 : 数组引用){
    System.out.println(变量名);
}


/**
 * 增强for循环 foreach
 */
public class ForeachArray {
    public static void main(String[] args) {
        //静态初始化定义字符串数组
        String[] students = {"Bobs", "Steven", "Kobe"};
        for (String student : students)
            System.out.print(student + "  ");
        System.out.println();
        int[] ages = {12, 18, 19};
        for (int age : ages)
            System.out.print(age + "  ");
        System.out.println();
        //布尔型数组
        boolean[] flags = {true, false, false, true, false};
        for (boolean flag : flags)
            System.out.print(flag + "  ");
    }
}

命令行参数

public class TestMain {
    public static void main(String[] args) {
        System.out.println("测试调用main方法");
        System.out.println(args.length);
        System.out.println(args[0]);
        System.out.println(args[1]);
    }
}

idea上调用给main方法传参数设置

img

dos窗口给main方法传参

javac TestMain.java

java TestMain 参数1 参数2 参数3 参数4 参数5 ........

排序与查找

冒泡排序

public static void bubbleSort(int[] arr) {
    //控制循环多少次
    for(int i = 0; i < arr.length; i++) {
    //循环找到余数组中最大的值
        for(int j = 1; j < arr.length-i; j++) {
            //交换元素的值
            if(arr[j] < arr[j-1]) {
                int temp = arr[j-1];
                arr[j-1] = arr[j];
                arr[j] = temp;
            }
        }
    }
}

选择排序

public static void selectsort(int[] arr) {
    for (int i = 0; i < arr.length-1; i++){
        int minindex = i;
        for (int j = minindex + 1; j < arr.length; j++){
            if (arr[j] < arr[minindex]){
                min = j;
            }
        }
        int temp = arr[i];
        arr[i] = arr[minindex];
        arr[minindex] = temp;
    }
}

线性查找

遍历数组,逐个比较,直到找到需要的元素就返回

二分查找

/**
 * int [] a = {2,1,6,5,8,3,7};
 * System.out.println(binaryFind(1,a,0,a.length-1));//调用
 * 递归方法二分查找
 * @param a 需要查找的数
 * @param arr 查找源
 * @param star 开始下标,默认为0
 * @param last 最后下标,默认array.length-1
 * @return true or false
 */
public static boolean binaryFind(int a, int [] arr , int star , int last){
    int mid = (star+last)/2;
    System.out.println("查找的位置:array["+mid+"]");
    
    if(arr[mid] == a)
        return true;
    else{
        if( mid-1 >= 0 && arr[mid-1] >= a){
            return binaryFind(a , arr ,star , mid-1);
        }
        else if (arr[mid+1] <= a){
            return binaryFind(a , arr ,mid+1 , last);
        }
    }
    return false;
}

作业

复习今天的内容。课上的代码最好都能自己实现一遍。

  1. 键盘输入1-12任意一个数,然后程序打印输出该月份对应的英文单词。禁止使用条件分支语句实现。
  2. 移动元素,把数组第一个元素移动到数组最后一个位置上。保证数组内的内容完整。
  3. 编写一个程序 ,找到大于所有项平均值的那些项。
  4. 输入一个不大于8位的数字判断这个数是不是回文数字。提示:所谓"回文",就是正着读和反着读是一样的
System.out.println("\r请输入一个小于八位数的数");
String string = scanner.next();
//反转字符串
//String reverse = new StringBuffer(string).reverse().toString();
//字符串转换为数组
char[] arr = string.toCharArray();
String reverse = "";
//进行字符串拼接
for (char i : arr) reverse = i + reverse;
if (string.equals(reverse)) System.out.println(string+"是回文数字");
else System.out.println("不是回文");



public static void huiwen(String n){
    char[] chars = n.toCharArray();
    a:
    for (int i=0;i<chars.length;i++){
        if (chars[i]!=chars[chars.length-i-1]) {
            System.out.println("不是回文");
            return;
        }else{
            continue a;
        }
    }
    System.out.println("是回文");
}//对还是错?for循环无意义,无论如何只进入一次循环   现在ok不?

二维数组

二维数组每一个元素都是一个一维数组。逻辑上可以把二维数组想象为一个表格

二维数组语法结构:

数据类型[][] 数组名 = {{值}, {值}, {值}};
数据类型[][] 数组名 = new 数据类型[二维数组长度][一维数组长度];
一维数组的长度不是必须的,但二维数组的长度必须要填写。

img

for (int i=0;i<array.length;i++){
    for (int j=0;j<array[i].length;j++){
        System.out.print(array[i][j]+"  ");
    }
}

//曾昭洋
int[][] arr = {{1,2,3,4,5,6,7},{10,12,13,14,15,16,17},{100,22,23,24,25,26,27}};
for (int i = 0; i < arr.length; i++) {
    arr[i][0] *= 2; //简单修改一个值
    for (int arrs:arr[i]) System.out.print(arrs + " "); //遍历全部
    System.out.println();
}

杨辉三角

public static void main(String [] args){
    int [][] arr1 = new int[7][];
    tcArray(arr1);
    printyhArray(arr1);

}
/**
 * 填充数组
 * @param array
 */
public static void tcArray(int [][] array){
    for(int i=0; i < array.length; i++){
        array[i] = new int[i+1];
        for(int j=0; j<= i; j++){
            if(j==0 || i==j)
                array[i][j] = 1;
            else if(j > 0 && i > j){
                array[i][j] = array[i-1][j]+ array[i-1][j-1];
            }
        }
    }
}
/**
 * 打印数组
 * @param array
 */
public static void printyhArray(int [][] array){
    for(int i=0; i < array.length; i++){
        for(int j = array.length - i -1 ; j > 0; j--){
            System.out.print("\t");
        }
        for( int j = 0; j<array[i].length; j++){
            //if(array[i][j] != 0 )
                System.out.print(array[i][j]+"\t\t");
        }
        System.out.println();
    }
}

img

商品管理系统

public static void kaiguang() {
    System.out.println("                       _oo0oo_");
    System.out.println("                      o8888888o");
    System.out.println("                     88\" . \"88");
    System.out.println("                      (| -_- |)");
    System.out.println("                     0\\  =  /0");
    System.out.println("                    ___/`---'\\___");
    System.out.println("                  .' \\|     |// '.");
    System.out.println("                / _||||| -:- |||||- \\");
    System.out.println("               |   | \\\\\\  -  /// |  |");
    System.out.println("               | \\_|  ''\\---/''  |_/ |");
    System.out.println("               \\  .-\\__  '-'  ___/-. /");
    System.out.println("             ___'. .'  /--.--\\  `. .'___");
    System.out.println("          .\"\" '<  `.___\\_<|>_/___.' >' \"\".");
    System.out.println("         | | :  `- \\`.;`\\ _ /`;.`/ - ` : | |");
    System.out.println("         \\  \\ `_.   \\_ __\\ /__ _/   .-` /  /");
    System.out.println("     =====`-.____`.___ \\_____/___.-`___.-'=====");
    System.out.println("                       `=---='");
    System.out.println("                                                ");
    System.out.println("                                                ");
    System.out.println("     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~         ");
    System.out.println("                    佛祖保佑         永无BUG         ");
}

七、面向对象

概念

英文:Object Oriented Programming(OOP)

OOP : 使用对象构造应用程序。

对象

万事万物皆对象。

对象具有的行为特征

特征:程序里的属性

行为:程序里的方法

抽象同一种对象的共有属性和行为,形成一个类。类是抽象的,对象是具体的。

人是一种抽象,但是具体的某个人,如:cxk就是一个对象。詹姆斯就是一个具体的对象。

如何通过程序来表现类。

语法格式:关键字<public, protected, default, private>` 关键字<class> 抽象事物的名字{}`

  • 比如:我们要类的概念抽象人 :public class Person {},此时,Person就是一个类
  • ​ 类的概念抽象车子 : public class Vehicle {} Vehicle也是一个类
  • ​ 类的概念抽象动物 : public class Animal {} Animal是一个类
  • ​ 类的概念抽象商品:public class Product{}, Product即是一个类
  • ​ 类的概念抽象细胞:public class Cell{}

类在程序中怎么表现:

/**
 * 用程序表现人这种抽象。
 */
public class Person {
    //特征:人这种抽象具有什么特性,也就是属性
    String name;//每个人都有姓名
    boolean gender;//每个人都有性别
    int age;//每个人都有年龄
    String address;//每个人都有一个住址
    String role;//每个人都扮演着自己的角色

    //行为: 人这种抽象具有哪些行为,也就是方法
    void eat(){}//每一个具体的人都会吃饭
    void sleep(){}//都会睡觉
    void playGame(){}//都会玩游戏
}

暂时无法在飞书文档外展示此内容

类和对象的关系

类是抽象,对象是具象。类是一个模板或叫做蓝本,而对象就是根据该模板创建出来的一个具体的事物。如房屋建造图纸是一个模板,蓝本,可以叫做类,而根据图纸这种模板建起来的一栋栋房屋就是一个个的对象。

类与对象之间的关系图:

暂时无法在飞书文档外展示此内容

创建对象

java里使用关键字new来创建对象。创建对象必须基于类。要创建对象必须先定义类。

java里创建对象:new 类名();

创建对象并赋值给某个引用:类名` 变量名(也叫引用) = `new 类名();

在实际代码中的表示:

/**
 * 基于类创建对象
 */
public class Test {
    public static void main(String[] args) {
        //基于类Person,使用new关键字,创建具体的一个个对象
        Person Bobs = new Person();//创建了一个名叫Bobs的人
        Person Jobs = new Person();//创建了一个名叫Jobs的人。
        //注意:以上的两个人能Bobs和Jobs,虽然都是基于类Person创建,但他们不是同一个对象。
    }
}

以上代码示例在内存中的布局图

暂时无法在飞书文档外展示此内容

实例

一个对象就是一个实例。常用术语:实例化一个对象。

实例变量

属于某个对象的变量就是实例变量。实例变量只能通过实例<对象>去访问。

在一个类里,怎么区分实例变量?没有被static修饰的属性就是实例变量或叫做成员变量。

代码示例:

/**
 * 类里的实例变量 
 */
public class Person {
    //特征:人这种抽象具有什么特性,也就是属性
    public String name;//每个人都有姓名
    public boolean gender;//每个人都有性别
    public int age;//每个人都有年龄
    public String address;//每个人都有一个住址
    public String role;//每个人都扮演着自己的角色
}

要访问以上代码里的实例变量,就必须要创建对象或叫做创建实例。

创建对象:Person Bobs = new Person();

通过实例访问实例内的属性代码

public static void main(String[] args) {
    //基于类Person,使用new关键字,创建具体的一个个对象
    Person bobs = new Person();//创建了一个名叫Bobs的人
    Person job = new Person();//创建了一个名叫Jobs的人。
    //注意:以上的两个人能Bobs和Jobs,虽然都是基于类Person创建,但他们不是同一个对象。
    //此处Bobs和Job是两个不同实例,我们可以使用该实例访问属于该实例内的属性和方法
    System.out.println(bobs.name);//访问实例变量name
    System.out.println(bobs.address);//访问地址
    //给实例里的属性赋值
    bobs.address = "延安中路18";
    System.out.println(bobs.address);
    //是否可以使用类名访问实例属性,
    //System.out.println(Person.name);访问时会报以下错误信息。
    //Non-static field 'name' cannot be referenced from a static context
}

实例变量和局部变量

实例变量:定义在类里、方法外、代码块外部的变量,并且没有被static修饰的变量。也叫成员变量,有时也可叫全局变量非静态变量,也叫参数。

局部变量:定义在代码块内、方法体内部以及方法的参数列表里的变量都是局部变量。

代码示例

/**
 * 实例变量和局部变量
 */
public class Animal {
    //定义成员变量
    String name;//名字
    String gender;//性别

    //定义局部变量, 首先需要定义一个方法
    public void eat(int amount) {//amount表示动物吃多少==数量
        String time;//动物每天几点开始吃
    }
}

实例变量和局部变量有什么区别:

  • 默认值:实例变量的默认值是该种数据类型的默认值,如String是null, int是0等
  • 局部变量分为方法的形参局部变量和方法体的局部变量。形参:因为在调用方法时必须要填入参数,所以无法测试其默认值。方法体内的局部变量必须要在定义时初始化。

作用域:实例变量的作用域是整个对象。局部变量作用域仅限于定义该变量的方法中

实例方法

同实例变量一样,没有被static修饰的方法就是实例方法,也叫成员方法。成员或实例方法必须要通过实例或对象才能访问。语法格式:引用.方法名() 形式访问

代码示例

/**
 * 实例方法
 */
public class Animal {
    //定义一个实例方法
    public void eat(int amount) {//amount表示动物吃多少==数量
        String time = null;//动物每天几点开始吃
        System.out.println(amount);
    }
    
    //main方法因为被static修饰,所以他是一个静态方法,属于类。
    public static void main(String[] args) {
        //访问类Animal里的实例属性
        //第一步创建Animal对象
        Animal dog = new Animal();
        //dog就是它的一个实例, 调用该实例的eat方法: 引用.方法名()  形式访问
        dog.eat(103);
    }
}

类变量和类方法

static关键字修饰的方法和属性叫做`方法`变量,static还可以用来修饰代码块

语法结构

修饰 属性/变量 : static 数据类型 变量名,修饰变量时只能用来修饰全局变量,修饰的变量叫做全局静态变量

修饰方法 :public static 数据类型 方法名(形参列表){//方法体},此时叫做静态方法

修饰代码块:static {//代码}

/**
 * 静态变量和静态方法以及静态代码块
 */
public class StaticKeyWords {
    //static 修饰属性 注意:static只能用来修饰全局属性
    public static String name;
    public static int age;    

    //static 修饰方法
    public static void main() {
        //static int a; 不允许定义局部的静态变量
        System.out.println("静态方法main()-----");
    }

    public static void me() {
        System.out.println("静态方法me()-----");
    }

    //static 修饰代码块,会在类被加载到java运行时内存里的方法区时执行
    static {
        System.out.println("静态代码块------");
    }
}

使用静态的类变量和方法

直接使用类调用的语法格式:类名.属性名 类名.方法名

public static void main(String[] args) {
    //访问StaticKeyWords类里的静态属性
    System.out.println(StaticKeyWords.name);
    System.out.println(StaticKeyWords.age);

    //访问StaticKeyWords类里的静态方法
    StaticKeyWords.main();
    StaticKeyWords.me();
}

使用对象访问类里的静态属性和静态方法

语法结构:对象引用.属性名 对象引用.方法名

public static void main(String[] args) {
    //创建类StaticKeyWords的对象
    StaticKeyWords staticKeyWords = new StaticKeyWords();
    //访问该类里的静态属性
    System.out.println(staticKeyWords.name);
    System.out.println(staticKeyWords.age);
    //访问该类里的静态方法
    staticKeyWords.main();
    staticKeyWords.me();
}

使用类访问和使用对象引用访问的区别

仅仅只是访问方式不同而已,被static修饰的属性和方法还是属于这个类。

在静态方法里使用类访问实例属性和实例变量:java不支持该种访问方式

总结

成员变量和成员方法只能通过类的对象去访问

静态全局变量和静态方法可以通过类名访问,也可以通过类的对象引用去访问。

暂时无法在飞书文档外展示此内容

构造方法

构造方法用来创建对象和给类里的属性赋值。

语法结构:修饰符<public, private> 类名() {}

此处的类名就是方法名,但是不能随意改变,必须要和类的名字保持一致。此处的类名就是当前类的名字.

/**
 * 构造方法 修饰符<public, private> 类名() {}
 */
public class ContructorMethod {
    //定义构造方法
    public ContructorMethod() {
        System.out.println("创建对象");
    }

    public static void main(String[] args) {
        /**
         * 创建类ContructorMethod的对象,会调用类的默认构造方法
         * 显示调用类ContructorMethod里的构造方法ContructorMethod();
         * 当类ConstructorMethod里没有显示定义ConstructorMethod()构造方法时类会默认添加此方法。
         */
        new ContructorMethod();
    }
}

每个类都有一个默认构造方法,此构造方法没有任何参数。创建对象时如果不跟上参数,都是在调用默认构造方法。默认构造方法的格式是:public 类名() {}。

调用构造方法的语法格式:new关键字加上构造放方法----- new ContructorMethod ();

构造方法没有显示的返回值,因为全部的构造方法都会默认返回创建出来的对象。

构造方法重载

构造方法重载可以按照普通方法重载去理解.

方法重载:方法名相同,方法形参的数据类型,个数,顺序不同就满足方法重载。

/**
 * 实例方法
 */
public class Animal {
    public String name;
    public Animal() {
        System.out.println("无参构造方法");
    }
    /**
     * 当类里定义了带参数的构造法时,默认构造方法就会失效。想要默认生效,就需要显示定义。
     * @param val
     */
    public Animal(String val) {
        name = val;
        System.out.println("name : " + name);

    }
    public static void main(String[] args) {
        //创建对象
        Animal animal = new Animal();//这句话会调用Animal类的默认构造方法
        Animal bobs = new Animal("HEllo");//调用有构造方法
    }
}

练习

1、把书抽象成一个类,然后基于该类创建各种具体的书对象,完成下列需求:

a、通过构造方法给对象里的参数赋值。

b、调用类的静态方法卖书

c、调用具体书对象的修改方法完成书简介的修改

书应该具有属性:书名、作者、简介、出版社、日期、价格等

public class Books {
    //书具有的属性
    public String name;
    public String introduction;
    public String author;
    public double price;
    //创建构造方法  public Books(){}默认构造方法,对象自带的。
    public Books(){
    }
    //创建有参构造方法  public Books(参数列表),一旦类里定义了有参构造方法,那么默认构造方法会失效
    public Books(String nm, String intro, String auth, double money) {
        name = nm;
        introduction = intro;
        author = auth;
        price = money;
    }
    //卖书
    public static void saleBook(String name) {
        System.out.println("卖书《" + name + "》成功");
    }
    //定义方法修改书的简介, 需要告诉该方法你要修改哪本书。因此需要传递参数books。
    public void modifyIntroduction(Books books, String info) {
        books.introduction = info;
    }

    //打印对象的信息,需要给方法传递对象
    public void show(Books book) {
        System.out.println(book + "对象的信息如下:");
        System.out.print("{name : " + book.name);
        System.out.print(", introduction : " + book.introduction);
        System.out.print(", author : " + book.author);
        System.out.print(", price : " + book.price + "}\n");
    }

    public static void main(String[] args) {
        //访问类Books里的成员变量,需要创建对象
        Books thinkInJava = new Books();//调用类Books里的默认构造方法
        //通过 引用.属性名 的方式给对象thinkInJava里的属性初始化
        thinkInJava.author = "Bruce Eckel";
        thinkInJava.introduction = "java 入门神书";
        thinkInJava.name = "Think In Java";
        thinkInJava.price = 10.0;
        //调用方法show打印对象的信息
        thinkInJava.show(thinkInJava);
        thinkInJava.saleBook(thinkInJava.name);

        //通过有参构造方法给对象的属性 赋值/初始化
        Books csapp = new Books(
                "深入理解计算机系统",
                "很浅的介绍了OS",
                "Bryant",
                68.8);
        //调用打印方法输出书本信息
        csapp.show(csapp);
        //调用卖书的方法需要给方法传递书的名字。
        csapp.saleBook(csapp.name);
        csapp.modifyIntroduction(csapp, "niubi");
        csapp.show(csapp);
    }
}

2、抽象用户这个概念来创建一个类User。此类里包含用户信息:用户名,密码,年龄,地址,性别等。

a、使用构造方法初始化用户信息

b、定义一个方法判断用户是否注册过。

c、定义一个静态方法判断用户是否成年。

d、定义一个方法允许用户开始玩游戏,玩游戏的前提是该用户必须注册过和成年。成年必须要通过调用是否

成年方法来判断。

构造方法,构造方法重载,实例方法,实例方法重载,静态方法,静态方法重载代码示例

/**
 * 构造方法,构造方法重载,实例方法,实例方法重载,静态方法,静态方法重载。
 */
public class User {
   //定义成员变量,也叫做实例变量,全局变量
   public String username;
   public String password;
   public int age;
   public boolean gender;
   public double price;
   //一个人的住址通常不轻易改变,所以把住址定义为静态变量
   public static String address;

   //定义无参默认构造方法,也叫构造函数
   public User(){}
    //定义有参构造方法,初始化对象的名字和密码
   public User(String un, String pwd) {
      username = un;
      password = pwd;
   }
   //重载构造方法初始化用户的年龄和性别
   public User(int year, boolean sex) {
      age = year;
      gender = sex;
   }

   //定义用户注册的方法
   public void register() {
      System.out.println("注册成功");
   }

   //定义游客用户登录的方法
   public void login() {
      System.out.println("游客登录成功");
   }
   //重载登录方法实现根据用户名和密码登录
   public void login(String username, String password) {
      System.out.println("用户[" + username + "]登录上来了");
   }

   //定义静态方法,也叫做类方法
   public static void getAddress() {
      System.out.println("用户的住址是 : " + address);
   }


   //无参修改住址静态方法
   public static void modifyAddress(){
      System.out.println("无参修改用户地址方法");
   }
   //重载修改用户住址的静态方法
   public static void modifyAddress(String newAddr) {
       address = newAddr;
   }

   //实现一个方法打印指定对象的信息
   public static void show(User user) {
      System.out.println("用户[" + user.username + "]的个人信息是:");
      System.out.print("{username:" + user.username);
      System.out.print(", password:" + user.password);
      System.out.print(", age:" + user.age);
      System.out.print(", gender:" + user.gender);
      System.out.print(", price:" + user.price + "}\n");
   }
}

public class TestUser {
    public static void main(String[] args) {
        //要使用类User的构造方法和成员变量以及成员属性,需要基于该类创建对象
        User hanmeimei = new User();//调用无参构造方法
        //调用注册方法
        hanmeimei.register();
        //调用登录方法
        hanmeimei.login();
        //使用用户名密码登录
        hanmeimei.login("韩梅梅", "12345678");
        //使用带参数的构造方法实例化一个用户
        User james = new User("詹姆斯", "james");
        james.gender = true;
        james.age = 18;
        //修改用户住址,注意modifyAddress是一个静态方法
        james.modifyAddress("洛杉矶");
        //打印对象james的信息
        User.show(james);
        //查看用户james的住址
        james.getAddress();
        //查看用户hanmeimei的住址
        //此处hanmeimei的地址也是洛杉矶,是因为User类里的地址变量是一个静态属性。
        //静态的都是属于类的,只会保持一份。
        hanmeimei.getAddress();
    }
}

对象创建内存简图

public class Person {
    public String gender = "男";
}
public class TestPerson {
    public static void main(String[] args) {
        Person bob = new Person();
        Person tom = new Person();
        System.out.println(bob.gender);
        System.out.println(tom.gender);
    }
}

img

this

this,代表指向对象自身的一个引用,在对象被创建时自动生成。this只能在对象里使用。不能在静态方法和静态代码块中使用this关键字。只要有对象就必定会有该对象对应的this引用, this分配在java运行时数据区域的栈区,它属于方法里的形式参数,具体解释可参考该文章:https://stackoverflow.com/questions/29472208/this-keyword-working-mechanism-in-java

注意:使用时用的是哪个对象引用调用方法,this就代表该引用对象。

/**
 */
public class Person {
    public String gender = "男";

    static {
        //System.out.println(this.gender);
    }
    public Person() {
        System.out.println(this.gender);
    }
    public void m() {
        System.out.println(this.gender);
    }
    public static void m2() {
        //System.out.println(this.gender);
    }
}
public class TestPerson {
    public static void main(String[] args) {
        new Person().m();
        //System.out.println(bob.gender);
    }
}

this初始化对象属性

/**
 * 使用this帮助初始对象的属性
 */
public class Person {
    public String username;
    public int age;
    public String address;
    public static String gender = "男";

    public Person() {
        System.out.println("无参构造方法被调用");
    }
    /**
     * 定义有参构造方法,初始化对象的属性
     */
    public Person(String username, int age, String address) {
        this.username = username;
        this.age = age;
        this.address = address;
    }

    /**
     * 打印对象属性
     */
    public static void show(Person obj) {
        System.out.println("用户[" + obj.username + "]的信息是:");
        System.out.println("{username:" + obj.username +
                ", age:" + obj.age +
                ", address:" + obj.address +
                ", gender:" + gender + "}");
    }
}
public class TestPerson {
    public static void main(String[] args) {
        Person bob = new Person("Bobs", 20, "阿三发射点发射点");
        //调用show方法打印对象bob的信息
        Person.show(bob);
        //改变对象bob的性别
        bob.gender = "人妖";

        //基于类Person创建对象
        Person tom = new Person();
        //调用方法show,打印对象tom的属性值
        Person.show(tom);
    }
}

案例

把数组章节的商品管理系统改成学生管理系统。支持添加、删除、修改、查询等功能。要求存储学生的基本信息,如:姓名、年龄、性别、名族、地址、学历、学校等。

package(包)

在大型项目开发的过程中,为了避免类名冲突,java引入了包的概念。

package必须要写在java文件的第一行。包名的命名方式约定俗成按照公司域名反过来。文件里的反斜线使用点来代替,如com\yangshun, com.yangshun

package com.yangshun;
public class Dog {
        public static void main(String[] args) {
                System.out.println("杨顺的狗");
        }
}

import(导入)

导入包。

import com.yangshun.Dog;
public class Cat {
        public static void main(String[] args) {
                //要使用一个类,直接创建该类的一个对象即可.
                Dog dog = new Dog();
                System.out.println("导入dog成功");
        }
}

访问修饰符

在java中,访问修饰符用于设置类,变量,方法,构造方法的可见性。

java中的修饰符有四个,分别是private, default, protected, public

修饰符本类本包不同包子类其他中文翻译
private 私有的
default 默认的
protected 受保护
public公共的

在实际项目开发中,使用最多的是private和public,尤其是在后边学习封装时,会大量使用private以及public,其他两个修饰符作为了解。我们自己写代码很少使用到。

如果变量,方法没有使用任何修饰符修饰,那么默认就是default。

  1. 当方法、变量被private修饰时,就不能在当前类之外访问它们。
  2. 如果我们没有为方法,变量显示指定任何访问修饰符,那么默认情况下会使用default来修饰它们
  3. 当方法、变量被protected修饰时,可以在同一个包以及子类中访问它们
  4. 当方法、变量被public修饰时,我们可以在任何地方访问他们。

三大特性

继承

什么是继承

继承是面向对象(OOP)的关键特性之一。它允许我们从一个现有的类直接创建新的类。达到代码的复用。创建的新类称为子类或派生类。和派生出子类的类叫做父类或超类以及基类。java中使用关键字extends来实现继承功能。注意:java只支持单继承。就是一个子类只能有一个父类。但是一个父类可以有多个子类。java中不能实现多继承,要达到多继承的功能可以借助后边学习的接口来实现。

继承语法结构:public class 子类名 extends 父类名 {}

程序实例

public class Animal {}
public class Dog extends Animal{}

上例中 Dog类通过关键字extends来实现继承Animal类。在这里,Dog是子类。Animal是父类。

为什么要用继承

代码复用,继承共性(类的属性,方法等)。可用以借用继承在java中实现多态。

程序示例

/**
 * 父类,超类。抽象所有的动物,动物都具有名字,性别等特征。都具有吃饭和睡觉等行为
 */
public class Animal {
    public String name;
    public String gender;

    public void eat(String name) {
        System.out.println(name + "在吃饭");
    }

    public void sleep(String name) {
        System.out.println(name + "在睡觉");
    }
}
public class Dog extends Animal {
    //因为Dog继承了Animal,所以Dog就拥有了Animal中的属性和方法。
    public void bite() {
        System.out.println("咬住十分钟");
    }

}
public class Cat extends Animal{
    //因为Dog继承了Animal,所以Dog就拥有了Animal中的属性和方法。

    //注意:只要代码里写了有参构造方法,就一定要显示书写默认无参构造方法
    public Cat() {}

    public Cat(String name, String gender){
        this.name = name;
        this.gender = gender;
    }

    public void hoots() {
        System.out.println("猫叫");
    }
}

public class TestAnimal {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.gender = "母狗";
        dog.name = "舔狗";
        System.out.println("狗的名字 : " + dog.name);
        System.out.println("狗的性别 : " + dog.gender);
        dog.eat(dog.name);
        dog.sleep(dog.name);
        dog.bite();//调用狗特有的方法

        //使用构造方法初始化类的属性
        Cat cat = new Cat("加菲", "公");
        System.out.println("猫的名字 : " + cat.name);
        System.out.println("猫的性别 : " + cat.gender);
        cat.eat(cat.name);
        cat.sleep(cat.name);
        cat.hoots();//调用猫独有的方法
    }
}

不可继承

  1. 构造方法不能被继承。
public class Animal {
    public String name;
    String gender;

    public Animal() {
        System.out.println("父类无参构造方法");
    }
}
public class Dog extends Animal {

    //子类的无参构造方法
    public Dog() {
        System.out.println("Dog的无参构造方法");
    }
}
public class TestAnimal {
    public static void main(String[] args) {
        Dog dog = new Dog();

    }
}
  1. 父类被private修饰的属性和方法不能被继承
/**
 * 父类,超类。抽象所有的动物,动物都具有名字,性别等特征。都具有吃饭和睡觉等行为
 */
public class Animal {
    public String name;
    private String gender;

    public void eat() {
        System.out.println("eat()-----");
    }
    private void sleep() {
        System.out.println("sleep()-----");
    }
}
public class Dog extends Animal {
    //子类里什么都不写,所有属性和方法都使用父类的。
}
public class TestAnimal {
    public static void main(String[] args) {
        //创建子类对象
        Dog dog = new Dog();
        //使用子类对象的引用区访问父类的属性
        //访问父类public修饰的属性
        System.out.println(dog.name);//能正常访问到
        //访问父类private修饰的属性。
        //会报错,因为父类的gender是用private修饰的,所以不能访问
        //System.out.println(dog.gender);

        //访问父类public修饰的方法
        dog.eat();//能正常访问到
        //访问父类private修饰的方法
        //dog.sleep();//不能访问到。
    }
}
  1. 父类中使用默认修饰符(default)修饰的属性和方法在不同包的子类中不能被继承。
package oop.inherit;
/**
 * 父类,超类。抽象所有的动物,动物都具有名字,性别等特征。都具有吃饭和睡觉等行为
 */
public class Animal {
    public String name;
    String gender;

    public void eat() {
        System.out.println("eat()-----");
    }
    void sleep() {
        System.out.println("sleep()-----");
    }
}

package oop;
import oop.inherit.Animal;
public class Duck extends Animal{
}

public class TestAnimal {
    public static void main(String[] args) {
        //创建子类对象
        Duck duck = new Duck();

        //使用子类对象的引用区访问父类的属性
        //访问父类public修饰的属性
        System.out.println(duck.name);//能正常访问到
        //访问父类默认的属性。
        //会报错,因为父类的gender是默认的,并且和父类不在同一个包下。所以不能访问
        //System.out.println(duck.gender);

        //访问父类public修饰的方法
        duck.eat();//能正常访问到
        //访问父类默认方法
        duck.sleep();//不能访问到。
    }
}

方法重写(Override)

概念:子类中有和父类相同方法签名和返回类型的方法称为方法的重写。

public class Animal {
    public void eat() {
        System.out.println("父类eat()");
    }
}
public class Dog extends Animal {
    //子类重写父类的eat方法。
    public void eat() {
        System.out.println("子类eat()----");
    }
}
public class TestAnimal {
    public static void main(String[] args) {
        //创建子类对象
        Dog dog = new Dog();
        //因为子类重写了父类方法,我们通过子类引用调用重写的方法时就不会去执行父类的方法。
        dog.eat();//访问子类重写父类的eat方法
    }
}

重写的特点:

方法的签名必须要相同:方法名相同、参数相同。返回值相同,子类访问修饰符至少要大于或等于父类访问修饰符。

public class Animal {
    public void eat(String name) {
        System.out.println("父类eat()");
    }
}
public class Dog extends Animal {
    //@Override此处eat方法虽然返回值和方法名都和父类相同,但是因为新参不一样,所以不是从写
    //此处加上重写注解@Override就会报错
    public void eat(int age) {
        System.out.println("子类eat()----");
    }
}

重写作用:父类中的方法无法满足子类的业务需求是,就需要重写父类的方法。

注意:重写时建议在子类的方法上加上注解@Override, 构造方法不能被重写,不能被继承的方法不能够被重写。

当子类重写父类方法和定义与父类相同属性时,在代码执行时,会使用子类自己定义的属性和重写后的方法。

public class Animal {
    private void eat() {
        System.out.println("父类eat()");
    }
}
public class Dog extends Animal {
    //此处方法不是从写,因为父类的eat方法是被private修饰的,所以子类不能重写。
    //因为不是从写,所以加上重写的注解Override会报错
    //@Override Method does not override method from its superclass
    public void eat() {
        System.out.println("子类eat()----");
    }
}

不能被重写的方法

被final关键字修饰的方法不能被子类重写

被static修饰的方法也不能被子类重写,但是可以在子类中再次声明

被private修饰的方法也不可以被重写

public class Person {
    public String name = "人类";

    /**
     * 被final关键字修饰的方法不能被子类重写
     */
    public final void study() {}

    /**
     * 被static修饰的方法也不能被子类重写,但是可以在子类中再次声明
     */
    public static void eat() {}

    /**
     * 被private修饰的方法也不可以被重写
     */
    private void run(){}
}
public class Man extends Person {
    @Override  
    public void study() {
        System.out.println("男孩学习java");
    }
    
    //@Override Method does not override method from its superclass
    public static void eat(){}
    
    //@Override Method does not override method from its superclass
    public void run(){}
}

重载(Overload)与重写(Override)

区别重写(Override)重载(Overload)
定义方法名相同、参数相同(参数类型、参数个数、顺序都要相同),返回值相同,子类访问修饰符至少要大于或等于父类访问修饰方法名相同,参数列表不同(参数类型、参数个数、顺序)
权限子类访问修饰符至少要大于或等于父类访问修饰对权限没有任何要求
范围发生在继承关系中发生在同一个类里

super关键字

this关键字代表当前对象,super关键字代表当前对象的父类对象。如果一个类有父类。那么当创建子类对象时会生成两个this和两个super,注意:创建哪个对象,生成的this和super引用就只能在哪个对象中使用。

super作用:可以在子类使用super调用父类的属性、方法、构造方法

使用super调用父类属性和方法

public class Animal {
    public String name;
    public void eat() {
        System.out.println("父类eat()");
    }
}
public class Dog extends Animal {
    public void m(String name) {
        //调用父类属性
        super.name = name;
        //调用父类方法eat()
        super.eat();
    }
}
public class TestAnimal {
    public static void main(String[] args) {
        //创建子类对象
        Dog dog = new Dog();
        //因为子类重写了父类方法,我们通过子类引用调用重写的方法时就不会去执行父类的方法。
        dog.m("土狗");
        System.out.println(dog.name);
    }
}

使用super调用父类构造方法

public class Animal {
    public String name;
    public Animal() {
        System.out.println("父类无参构造方法");
    }
    public Animal(String name) {
        this.name = name;
    }
}
public class Dog extends Animal {
    //定义无参构造方法
    public Dog() {
        //调用父类无参构造方法
        //当不显示写super()时虚拟机会默认添加上该调用
        //super()调用必须要写在构造方法内的第一行
        //调用父类有参构造方法
        super("斑点狗");
        System.out.println("子类无参构造方法");
        //super("斑点狗");必须要写到第一行,否则报错。
    }
}
public class TestAnimal {
    public static void main(String[] args) {
        //创建子类对象
        Dog dog = new Dog();
        System.out.println(dog.name);
    }
}

当创建一个对象时都会生成一个this和一个super引用

//父类
public class Animal {
    String name="爹";
    String gender;
    public  Animal(){
        System.out.println("这个是父类构造方法");
    }
    public void eat(String name){
        System.out.println(name+"吃草");
    }
}
//子类
public class cat extends Animal {
    public cat(String name ,String gender ){
        this.name=name ;
        this.gender=gender;
        System.out.println(name+gender);
    }
    public void jiao(String name){
        System.out.println(name+"开始喵喵叫"+super.name);
    }
    @Override
    public  void eat(String name){
        System.out.println(super.name+"吃鱼小猫咪都吃罐罐");
    }
}
//测试类
public class test {
   static cat c=new cat("kitey","girl");

   static  Animal animal=new Animal();

    public static void main(String[] args) {
        System.out.println(c.name+"  "+c.gender);
        System.out.println(animal.name);


        c.jiao(c.name);
        c.name="哆啦A梦";
        c.eat(c.name);

    }
}

子类对象创建过程

当类没有继承关系时的对象创建过程

  1. 当程序遇到代码中使用new指令时,就会把new关键字之后的类加载到内容,并开始创建对象
  2. 根据该类中定义的变量到内存中申请内存空间
  3. 实例化该类里的成员变量
  4. 执行该类的构造方法
  5. 将创建出来的对象的地址赋给一个变量,该变量就叫做引用

如下代码示例:

public class Animal {
    public String name;
}
public class TestAnimal {
    public static void main(String[] args) {
        //创建子类对象
        Animal animal = new Animal();
    }
}

暂时无法在飞书文档外展示此内容

当类有继承关系时对象的创建过程

  1. 先创建其父类的对象,实例化父类的成员变量。执行父类的构造方法.
  2. 创建子类对象。实例化子类的成员变量,执行子类的构造方法。

img

多态

概念

父类引用指向子类对象。从而产生多态。通俗理解,同一个方法调用,产生不同的输出结果。

public class Person {
    public String name = "人类";


    public void study() {
        System.out.println(name + "学习");
    }
}
public class Man extends Person {
    String name;
    public Man(){}
    public Man(String name) {
        this.name = name;
    }

    //重写父类的学习方法

    @Override
    public void study() {
        System.out.println("男孩学习java");
    }
    
  }
 public class Bobs extends Man{
    String name;
    public Bobs(){}
    public Bobs(String name) {
        this.name = name;
    }
}
public class Girl extends Person{
    String name;
    public Girl(String name) {
        this.name = name;
    }

    @Override
    public void study() {
        System.out.println("女孩学习HTML5");
    }
}
public class TestPerson {
    public static void main(String[] args) {
        invokeStudy(new Man("男孩"));
        invokeStudy(new Girl("女孩"));
        //Bobs类没有重写study方法
        invokeStudy(new Bobs("bobs"));
    }
    public static void invokeStudy(Person person) {
        //此处person就是父类引用,但具体执行代码时会根据传入的具体子类去执行子类的方法
        //从而产生多态
        person.study();
    }
}

多态产生的条件

  1. 必须要有继承
  2. 必须要有方法的重写
  3. 必须要有父类引用指向子类对象

多态的特点

  1. 如果发生多态,那么调用的一定是子类重写父类的方法
  2. 如果发生多态,父类引用是无法调用子类自己独有的方法和属性。可以使用使用向下转型来访问子类的独有属性和方法

向上转型和向下转型

当我们把一个子类对象赋值给父类引用时就是一种默认的向上转型,

语法结构:父类名 引用 = new 子类名(); 如:Person person = new Man();

向下转型,类似于强制类型转换中的把高位的数据赋值给低位时做的强制转换类似:

语法结构: 子类名 引用 = (子类名)父类引用;

int a = 10; short s;  s = (short) a;基本数据类型强制转换
把父类转成子类,就是从高到低的转换,我们称为向下转型
Person person = new Person();
Man man = (Man)person;

public class Person {
    public String name = "人类";

    public void study() {
        System.out.println(name + "学习");
    }
}

public class Man extends Person {
    String name;
    String laryngeal = "喉结";
    public Man(){}
    public Man(String name) {
        this.name = name;
    }
    //重写父类的学习方法
    @Override
    public void study() {
        System.out.println("男孩学习java");
    }

    public void run() {
        System.out.println("男孩会打球");
    }
}


public static void main(String[] args) {
    //向上转型
    Person person = new Man();
    person.study();
    System.out.println(person.name);//
    //System.out.println(person.laryngeal);//访问子类独有的属性
    //person.run();//父类引用不能访问子类独有的方法,当然属性也是不能访问的
    //如果非要访问子类独有的属性和方法,就需要把父类引用向下转型成子类对象
    //向下转型
    Man man = (Man)person;
    man.run();//此时可以访问子类独有的方法
    man.study();
    System.out.println(man.laryngeal);//访问子类独有的属性

}

instanceOf关键字

作用:判断某个对象是否属于某一种类型,属于就返回true,否则返回false;

语法结构:对象名/引用 instanceOf 类型

应用场景:在向下转型的过程中先使用它来判断需要被转的引用是否是目标类型。

Man man = null;
if (person instanceof Man) {
    man = (Man) person;
}

练习

学生喂养三种宠物:猫、狗、鸟

  1. 动物类(Animal):属性(name, age), 方法(speak, move, eat)
  2. 猫类(Cat)继承动物类,重写父类的方法。添加方法play();
  3. 狗类(Dog)继承动物类,重写父类的方法。添加方法play();
  4. 鸟类(Bird)继承动物类,重写父类的方法。添加方法play();
  5. 学生类(Student):属性(name), 方法(feed(Animal animal))
public class Animal {
    //动物的属性
    public String name;
    public int age;

    //行为---方法
    public void speak() {
        System.out.println("Animal speak");
    }

    public void move() {
        System.out.println("Animal move");
    }

    public void eat() {
        System.out.println("Animal eat");
    }
}
public class Cat extends Animal{

   //定义构造方法给属性赋值
   public Cat(String name, int age) {
      this.name = name;
      this.age = age;
   }

   //重写父类方法
   @Override
   public void speak() {
      System.out.println("Cat speak");
   }

   @Override
   public void move() {
      System.out.println("Cat move");
   }

   @Override
   public void eat() {
      System.out.println("Cat eat");
   }

   //定义子类独有的方法paly
   public void play() {
      System.out.println("Cat play");
   }
}
public class Dog     extends Animal{
    public Dog(String name, int age) {
       this.name = name;
       this.age = age;
    }
   //重写父类方法
   @Override
   public void speak() {
      System.out.println("Dog speak");
   }

   @Override
   public void move() {
      System.out.println("Dog move");
   }

   @Override
   public void eat() {
      System.out.println("Dog eat");
   }

   //定义子类独有的方法paly
   public void play() {
      System.out.println("Dog play");
   }
}
public class Bird extends Animal{
    public Bird(String name, int age) {
       this.name = name;
       this.age = age;
    }
   //重写父类方法
   @Override
   public void speak() {
      System.out.println("Bird speak");
   }

   @Override
   public void move() {
      System.out.println("Bird move");
   }

   @Override
   public void eat() {
      System.out.println("Bird eat");
   }

   //定义子类独有的方法paly
   public void play() {
      System.out.println("Bird play");
   }
}
public class Student {
    public String name;//学生名字

    //通过构造方法给属性赋值
    public Student(String name) {
        this.name = name;
    }

    //学生喂养宠物
    public void feed(Animal animal) {
        System.out.println(name + "养了一只" + animal.name + "已经" + animal.age + "岁了");
        animal.eat();
        animal.move();
        animal.speak();

        //调用子类独有play方法,就需要把父类向下转型
        if (animal instanceof Cat) ((Cat) animal).play();
        else if(animal instanceof Dog) ((Dog) animal).play();
        else if (animal instanceof Bird) ((Bird) animal).play();
    }
}
public class Test {
    public static void main(String[] args) {
        //创建一个学生,名字叫Steven
        Student student = new Student("Steven");
        student.feed(new Cat("Blue Cat", 3));
        student.feed(new Dog("Labrador", 5));
        student.feed(new Bird("bird", 12));
    }
}

静态绑定和动态绑定

概念

Java中有两种绑定方式,一种是静态绑定,又称作前期绑定。另一种是动态绑定,亦称为后期绑定。

两者的区别:

  • 静态绑定发生在编译时期(也就是javac阶段),动态绑定发生在运行时(就是执行java指令后)
  • 使用private或static或final修饰的变量或者方法,使用静态绑定。而被子类重写的方法则会根据运行时的对象进行动态绑定。
  • 静态绑定使用类信息来完成,而动态绑定则需要使用对象信息来完成。
  • 重载(Overload)的方法使用静态绑定完成,而重写(Override)的方法则使用动态绑定完成。
静态绑定
public class Test {
  public static void main(String[] args) {
      String str = new String();
          Person person = new Person();
          person.play(str);
  }

  static class Person {
      public void play(Object obj) {
          System.out.println("这是一个对象实例的play");
      }

      public void play(String str) {
          System.out.println("这是一个字符串实例的play");
      }
  }
}

//输出结果是:
//"这是一个字符串实例的play"
动态绑定

案例一:

class Person {

    protected String name = "我是person";

    public void method() {
        System.out.println("person method");
    }
}

class Boy extends Person {
    protected String name = "我是boy";

    public void method() {
        System.out.println("boy method");
    }
}

public class Test {
    public static void main(String[] args) {
        Person p = new Boy();
        System.out.println(p.name);
        p.method();
    }
}
//输出结果是:
//我是person
//boy method

案例二:

class Person {

    protected String name = "我是person";

    public void method() {
        System.out.println("person method");
    }
}

class Boy extends Person {
    protected String name = "我是boy";
}

public class Test {
    public static void main(String[] args) {
 
//1:编译器检查对象的声明类型和方法名。假设我们调用p.method()方法,
//    并且p已经被声明为Boy类的对象,那么编译器会列举出Boy类中
//    所有的名称为method的方法和从Boy类的父类继承过来的method方法
//2:接下来编译器检查方法调用中提供的参数类型。如果在所有名称为method的
//    方法中有一个参数类型和调用提供的参数类型最为匹配,那么就调用这个方法,
//    这个过程叫做“重载解析” 
//3:当程序运行并且使用动态绑定调用方法时,虚拟机必须调用同p所指向的对象的
//    实际类型相匹配的方法版本。假设boy类定义了mehod()那么该方法被调用,
//    否则就在Boy的父类(Person类)中搜寻方法method()
        Person p = new Boy();
        System.out.println(p.name);
        p.method();
    }
}

//输出结果是:
//我是person
//person method
总结:

1、子类的对象调用到的是父类的成员变量,动态绑定针对的范畴只是对象的方法,而属性采取静态绑定方式。 2、执行p.method()时会先去调用子类的method方法执行,若子类没有则向上转型去父类中寻找。 所以在向上转型的情况下,对象的方法可以找到子类,而对象的属性还是父类的属性。

多态总结

  • 定义: 在Java中,多态是指不同类的对象在调用同一个方法时所呈现出的多种不同行为。
  • 说明: 通常来说,在一个类中定义的属性和方法被其他类继承或重写后,当把子类对象直接赋值给父类引用变量时,相同引用类型的变量调用同一个方法所呈现出的多种不同形态。
  • 作用: 通过多态,消除了类之间的耦合关系,大大提高了程序的可扩展性和可维护性。
  • 注意: Java的多态性是由类的继承、方法重写以及父类引用指向子类对象体现的。由于一个父类可以有多个子类,多个子类都可以重写父类方法,并且多个不同的子类对象也可以指向同一个父类。这样,程序只有在运行时才能知道具体代表的是哪个子类对象,这就体现了多态性。

封装

什么是封装

现实中的封装就是把事物的细枝末节隐藏,对外提供一个统一的入口

在java的世界里,封装就是隐藏代码里的属性和一些不希望被用户访问到的方法。而提供一些公共的方法作为入口,供使用者通过这些入口来和我们的程序打交道,但是用户只能知道入口方法的名字以及方法有哪些参数,对于方法具体执行的逻辑还是不能暴露给用户。

一个程序示例:封装一个java程序,叫做Person.java

/**
 *程序的封装,所有属性都应该是封装起来,不允许用户直接访问。
 */
public class Person {
    private String name = "我是person";
    private String password = "sxxx";
    private int age = 18;

    //如果某些属性是希望被其他用户访问的,
    //那么可以通过提供方法的形式让其访问或修改。这就是针对属性的get和set方法
    /**
     * 获取用户名,也是需要通过方法的形式获取,避免使用引用.属性名 的方式访问
     * @return
     */
    public String getName() {
        return name;
    }

    /**
     * 修改用户名,通常都是通过set方法,而不允许用户直接修改属性
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
public class Test {
    public static void main(String[] args) {
        //创建一个Person对象
        Person person = new Person();
        person.setName("张三");
        //通过方法获取用户名字
        System.out.println(person.getName());
    }
}

为什么需要封装对象

因为在对象外部,为对象赋值,可能存在非法数据的输入。

但是就目前而言,是没有办法可以直接避免非法数据的输入的。即便把属性定义为private修饰,也不能完全屏蔽掉非法输入。用户还是可以通过反射技术访问对象内部private修饰的属性。不过把属性都设置为私有相对来说还是比较安全的一种做法。封装可以使我们的对象更安全的发布。通过封装我们可以把一个不是线程安全的对象变成一个线程的安全的。

在设计一个类时尽量做到以下几点

  1. 私有化属性,也就是把属性使用private修饰符修饰
  2. 对于这些属性,对外提供公共的get和set方法
  3. 提供有参无参构造方法

final关键字

final :java中的一个保留字。中文意思是:最后的,最终的,不可被修改的。

final关键字可以被用来修饰类,方法,属性。

final修饰类

当final修饰一个类时,表示此类已经是一个完整的,最终的类,功能完备,不需要再被改变,所以被final修饰类不能被继承。java中有许多类就是被申明为fianl的,如String,Integer。

/**
 * 演示被final修饰的类不可被继承
 */
public final class One {
}

/**
 * 该类继承One类
 */
public class Two extends One{//Cannot inherit from final 'oop.finalkeywords.One'
                            //不能从final 类One 继承
}

final修饰方法

final修饰方法时,此方法不能被覆盖,也即是不能被重写。

/**
 * 演示被final修饰的方法不能重写
 */
public class One {
    public final void m1() 
}
/**
 * 该类继承One类
 */
public class Two extends One{
    //重写父类的m方法,会报错
    public void m1(){}
}

final修饰属性

final修饰变量,只能显示赋值一次,此后该变量的值不能被改变。注意:系统默认赋值不算第一次赋值

public class Test {
    final int b;
    public Test(int b) {
        this.b = b;//被final修饰的成员变量可以延迟到构造方法里赋值
    }
    static final int c;
    static {
        c = 101;//静态的final类变量可以在静态代码块中赋值。
    }
    public static void main(String[] args) {
        //定义一个final变量
        final int a ;
        //修改final变量a的值
        a = 101;
        //修改全局成员变量b的值
        //b = 200;//报错原因是因为b已经是一个final类型的变量,并且被赋值过一次。
    }
}

当final修饰变量时,该变量就是常量,常量又分为静态常量,成员常量以及局部常量。

public class Test {
    //静态常量
    static final int a = 10;
    //成员常量
    final int b = 20;
    public static void main(String[] args) {
       //局部常量
       final int c = 30;
    }
}

final修饰引用类型

当final修饰的变量是一个引用时,那么该引用的地址内容不能再改变,而引用指向的那块内存空间中的值是可以修改的。

final修饰对象引用

当final修饰一个引用类型的变量时,所谓的不可变是指引用的地址不可变,而可变是指引用指向的具体内容可变。

public class TestFinalRef {
    public static void main(String[] args) {
        //创建一个Person对象,并把该对象赋值给一个final修饰的变量
        final Person person = new Person();
        //修改person引用的值
        //person = null; Cannot assign a value to final variable 'person'
        //修改person引用对象内部的属性
        person.setName("Bobs");
    }
}

final修饰数组变量

public class TestFinalRef {
    public static void main(String[] args) {
        final String[] arr = new String[10];
        //arr = null;因为arr已经被final修饰,所以引用地址不能再变
        arr[0] = "hello";//但引用指向的内容是可以被修改的。
    }
}

抽象类(abstract)

java中使用关键字abstract来定义抽象。当用来修饰类时,该类就是一个抽象类,修饰方法时,该方法就是抽象方法。

abstract只能用来修饰类和方法。

当我们创建父类时,可以预先知道某些方法是一定会被子类重写的。因此这些一定会被重新覆盖掉的方法在父类中就没有必要写具体的实现,应该该方法定义为抽象方法。java中规定,拥有抽象方法的类叫做抽象类。

特点:

有构造方法,不能创建对象。

public abstract class AbstractClassDemo {
    //定义构造方法
    public AbstractClassDemo() {

    }
}
public class TestAbstractClassDemo {
    public static void main(String[] args) {
        //创建抽象类对象
        AbstractClassDemo acd = new AbstractClassDemo();//会报错
    }
}

修饰类的语法结构:<public> abstract class 类名{}

public abstract class AbstractClassDemo {
    //类型可以不用写任何代码逻辑
}

修饰方法的语法结构:<public> abstract void 方法名();

public abstract class AbstractClassDemo {
    //类型可以不用写任何代码逻辑

    //定义一个成员抽象方法
    public abstract void m1();
    //定义一个静态抽象方法,不允许这样定义,会报错:Illegal combination of modifiers: 'abstract' and 'static'
    public abstract static void m2();
}

抽象类使用注意事项:

抽象类里可以没有抽象方法

public abstract class AbstractClassDemo {
    public void m1() {
        System.out.println("成员方法m1()");
    }
}

抽象类里可以同时具有抽象方法和普通方法

public abstract class AbstractClassDemo {
    public void m1() {
        System.out.println("成员方法m1()");
    }

     //抽象方法
    public abstract void m2();
}

如果一个类里包含了抽象方法,那么该类必须要被定义为一个抽象类,否则会报错。

如果子类不实现父类的抽象方法,那么子类也应该要被申明为抽象的类,否则报错。

总结:

抽象类更像是一种规范,它定一个统一的模版,让所有继承它的子孙都必须实现自己定义的模版。达到统一管理的目的,并且也可以实现向实际项目开发中的模块与模块之间代码的解耦。也是实现多态的关键要素之一。

面试题(java2022刘浪提供)

是不是所有没有方法体的方法都是抽象方法:答案:不是,比如native方法。

接口(interface)

接口就是一种标准,定义接口的过程,就是定义标准。

定义

接口相当于一种特殊的抽象类。定义方式、组成部分与抽象类类似。使用关键字interface来修饰

语法:public interface 接口名{}

public interface InterfaceDemo {
}

接口的特点:

没有构造方法,不能创建对象。

public interface InterfaceDemo {
    //定义构造方法
    //public InterfaceDemo(){},不允许这样使用
}
public class TestInterfaceDemo {
    public static void main(String[] args) {
        //创建接口对象
        //new InterfaceDemo();因为接口不允许有构造方法,所以必然不允许创建对象
    }
}

只能定义公共静态常量,公共抽象方法。在jdk8之后,新增可以在接口内部定义默认方法和静态方法。

public interface InterfaceDemo {
    //定义常量
    public static final int a = 10;
    //接口中的常量默认使用public static final修饰。所以可以省略不写
    int b = 101;

    //公共抽象方法
    public abstract void m1();


    //定义默认方方法
    default void m2() {        
        System.out.println("接口中的默认方法");
    }
    //静态方法
    static void m5() {
        System.out.println("接口中的静态方法");  
    }
}

接口的实现

java中使用关键字implements来实现一个接口,实现接口的子类必须要提供接口中抽象方法的实现

public class InterfaceDemoImple implements InterfaceDemo{
    public void m1() {
        System.out.println("实现接口中的方法");
    }
}

因为接口不允许创建对象,所以要访问接口中的默认方法时只能通过创建子类对象去访问

//通过创建子类对象去访问接口总的默认方法
new InterfaceDemoImple().m2();

接口和抽象类的区别

相同点:

都可以被编译为字节码文件

都不能创建对象

都可以作为引用类型

不同点:

接口中的所有属性都是被public static final修饰的常量。

接口中所有的方法都是public abstract修饰的。

接口没有构造方法。代码块和静态代码块。

接口使用注意事项

任何类在实现接口时,必须实现接口中所有的方法,否则该类就是一个抽象类

实现接口中的抽象方法时,访问修饰符必须是public修饰的

接口的好处

程序的耦合性降低

可以更好的实现多态

共容易设计程序的框架

共容易更换具体实现

设计与实现分离

接口和抽象类联合使用

我们都知道,java是不支持多继承,那么如果非要在java里实现多重,那就需要使用接口。

以下代码示例演示普通类,接口,抽象类三者之间继承和实现的各种用法

//一个类可以实现多个接口
class N implements B, C{}

//在Java中使用多继承,Z继承A,但同时也要继承B和C
class Z extends A implements B, C{}

//抽象类实现接口
abstract class X implements B, C{}

//接口与接口相互继承
interface T extends B{}

//接口是否可以实现接口
//interface H implements C{}//java不支持接口实现接口

//接口继承抽象类
//interface P extends A{}//java不支持这种继承关系

//写一个抽象类
abstract class A {}

//写两个接口
interface B{}
interface C{}

作业

  1. 使用接口或抽象类改造学生喂养宠物的练习
  2. 列出类与接口的区别
相同点:
    都可以被编译为字节码文件
    都可以申明公共的静态常量
不同点:
    接口中的所有属性都是被public static final修饰的常量。
    接口中所只能定义两种方法,第一种被public abstract修饰,并且没有实现。另一种是default修饰的方法
    接口没有构造方法。代码块和静态代码块。
  1. 列出抽象类和类的区别
相同点:
    都可以被编译为字节码文件
    都有构造方法和普通方法。
    都可以定义变量,代码块
不同点:
    抽象类必须要使用关键字abstract修饰
    抽象类不能创建对象。
    抽象类可以只定义方法体,而不需要写实现

设计模式-工厂模式

为什么要在此处讲解工厂模式?

关于面向对象的三大特性:继承、封装、多态,不管怎么背概念和文字举例子,其实都很难做到第一次学就对这三大特性有很好的理解。即便是编写一些dos窗口操作的管理系统demo,也很难融合三大特性的特点。但是工厂模式把面向对象的三大特性应用的非常好,因此,此处只要我们能把工厂模式理解,并且在学习完后能独自把这种设计模式用代码实现出来,我相信你会对面向对象的三大特性有另一种不一样的认识。并且工厂模式也被应用到各种主流框架,如大名鼎鼎的spring框架。所以学好工厂模式,不仅能帮助理解三大特性,还能进一步体会到Java编程思想美。

  • version-1.0,用户想订购披萨
  • 暂时无法在飞书文档外展示此内容
/**
 * 客户类,需要订购披萨的用户
 */
public class Customer {
    public static void main(String[] args) {
        //创建一个披萨店,调用披萨店的订购披萨方法完成披萨的订购
        PizzaStore pizzaStore = new PizzaStore();
        pizzaStore.orderPizza();
    }
}
public class PizzaStore {
    public Pizza orderPizza() {
        Pizza pizza = new Pizza();
        pizza.prepare();//准备
        pizza.bake();//烘烤
        pizza.cut();//切片
        pizza.box();//打包
        return pizza;
    }
}
public class Pizza {
    public void prepare() {
        System.out.println("准备");
        System.out.println("搅拌面团...");
        System.out.println("正在添加酱汁...");
        System.out.println("添加浇头: ");
    }

    public void bake() {
        System.out.println("在350℃下烘烤25分钟");
    }

    public void cut() {
        System.out.println("把披萨对角切成片");
    }

    public void box() {
        System.out.println("将披萨放在官方披萨店的盒子里");
    }
}

version-1.1,可以根据用户提供的口味提供不同的披萨给他们

public class Customer {
    public static void main(String[] args) {
        PizzaStore02 pizzaStore02 = new PizzaStore02();
        pizzaStore02.orderPizza("cheese");
    }
}
public class PizzaStore02 {
    public Pizza orderPizza(String type) {
        Pizza pizza = null;
        if (type.equals("cheese")) pizza = new CheesePizza();
        else if (type.equals("greek")) pizza = new GreekPizza();
        else if (type.equals("pepperoni")) pizza = new PepperoniPizza();
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();//装盒子
        return pizza;
    }
}
public interface Pizza {

    public abstract void prepare();

    default void bake() {
        System.out.println("在350℃下烘烤25分钟");
    }

    default void cut() {
        System.out.println("把披萨对角切成片");
    }

    default void box() {
        System.out.println("将披萨放在官方披萨店的盒子里");
    }
}
public class CheesePizza implements Pizza {
    public void prepare() {
        System.out.println("准备CheesePizza");
        System.out.println("搅拌面团...");
        System.out.println("正在添加酱汁...");
        System.out.println("添加浇头: ");
    }
}

暂时无法在飞书文档外展示此内容

version-2.0 简单工厂,把创建对象的过程交给工厂来完成

public class Customer {
    public static void main(String[] args) {
        PizzaStore03 pizzaStore03 = new PizzaStore03(new SimplePizzaFactory());
        pizzaStore03.orderPizza("cheese");
    }
}
public class PizzaStore03 {
    SimplePizzaFactory factory;
    public PizzaStore03(SimplePizzaFactory factory) {
        this.factory = factory;
    }
    public Pizza orderPizza(String type) {
        Pizza pizza = factory.createPizza(type);
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();//装盒子
        return pizza;
    }
}
public class SimplePizzaFactory {
    public Pizza createPizza(String type) {
        Pizza pizza = null;
        if (type.equals("cheese")) pizza = new CheesePizza();
        else if (type.equals("greek")) pizza = new GreekPizza();
        else if (type.equals("pepperoni")) pizza = new PepperoniPizza();
        return pizza;
    }
}

严格来说,简单工厂不是一个设计模式,更像是一种编程习惯

暂时无法在飞书文档外展示此内容

version-2.1 把createPizza移到PizzaStore类里

暂时无法在飞书文档外展示此内容

工厂方法用来处理对象的创建,并将这样的行为封装在子类中,这样测试程序或用户使用的永远都是超类,而真正创建对象的子类,客户接触不到,这就实现了解耦。

使用到的类

以上代码画出平行视角类关系

暂时无法在飞书文档外展示此内容

工厂方法让类把实例化推迟到子类

public class PizzaTestDrive {
    public static void main(String[] args) {
        AbstractPizzaStore nyStore = new NYPizzaStore();
        AbstractPizzaStore chicagoStore = new ChicagoPizzaStore();

        Pizza pizza = nyStore.orderPizza("cheese");
        System.out.println("Jackson Ordered a " + pizza.getName() + "\n");
        pizza = chicagoStore.orderPizza("cheese");
        System.out.println("James ordered a " + pizza.getName() + "\n");
    }
}
public abstract class AbstractPizzaStore {
    Pizza orderPizza(String type) {
        Pizza pizza = createPizza(type);
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();//装盒子
        return pizza;
    }

    public abstract Pizza createPizza(String type);
}
public class NYPizzaStore extends AbstractPizzaStore{
    @Override
    public Pizza createPizza(String type) {
        Pizza pizza = null;
        if (type.equals("cheese"))
            pizza = new NYStyleCheesePizza();
        else if (type.equals("pepperoni"))
            pizza = new NYStylePepperoniPizza();
        else if (type.equals("clam"))
            pizza = new NYStyleClamPizza();
        else if (type.equals("veggie"))
            pizza = new NYStyleVeggiePizza();
        return pizza;
    }
}
public class ChicagoPizzaStore extends AbstractPizzaStore{
    @Override
    public Pizza createPizza(String type) {
        Pizza pizza = null;
        if (type.equals("cheese"))
            pizza = new ChicagoStyleCheesePizza();
        else if (type.equals("pepperoni"))
            pizza = new ChicagoStylePepperoniPizza();
        else if (type.equals("clam"))
            pizza = new ChicagoStyleClamPizza();
        else if (type.equals("veggie"))
            pizza = new ChicagoStyleVeggiePizza();
        return pizza;
    }
}

public abstract class Pizza {
    String name;
    String dough;
    String sauce;
    ArrayList toppings = new ArrayList();
    public void prepare() {
        System.out.println("准备 " + name);
        System.out.println("搅拌面团...");
        System.out.println("正在添加酱汁...");
        System.out.println("添加浇头: ");
        //此处逻辑照抄就行
        for (int i = 0; i < toppings.size(); i++) {
            System.out.println("   " + toppings.get(i));
        }
    }

    public void bake() {
        System.out.println("在350℃下烘烤25分钟");
    }

    public void cut() {
        System.out.println("把披萨对角切成片");
    }

    public void box() {
        System.out.println("打包。。。。。。");
    }

    public String getName() {
        return name;
    }
}
public class NYStyleCheesePizza extends Pizza{
    public NYStyleCheesePizza() {
        name = "NY Style Sauce and Cheese Pizza";
        dough = "Thin Crust Dough";
        sauce = "Marinara Sauce";
        //toppings.add("Grated Reggiano Cheese");
    }
}
请自己实现:
NYStylePepperoniPizza.java, 
NYStyleClamPizza.java, 
NYStyleVeggiePizza.java

public class ChicagoStyleCheesePizza extends Pizza{
    public ChicagoStyleCheesePizza() {
        name = "Chicago Style Deep Dish Cheese Pizza";
        dough = "Extra Thick Crust Dough";
        sauce = "Plum Tomato Sauce";
        toppings.add("Shredded Mozzarella Cheese");
    }

    public void cut() {
        System.out.println("Cutting the pizza into square  slices");
    }
}
也请你自己实现:
ChicagoStylePepperoniPizza.java   
ChicagoStyleClamPizza.java  
ChicagoStyleVeggiePizza.java

一个完整的类内部可以编写的代码模块

程序的执行顺序:①静态代码块②动态代码块③构造方法。并且静态代码块只会被执行一次。

public class CompleteClass {
    //属性
    private String username;
    private static int age;
    //定义常量
    public static final String GENDER = "男";
    public final String address = "男";
    //代码块
    {
        System.out.println("动态代码块");
    }
    static {//静态代码块只会执行一次。
        System.out.println("静态代码块");
    }

    //构造方法
    //无参数,默认
    public CompleteClass(){
        System.out.println("无参构造方法");
    }
    //有参数
    public CompleteClass(Person person) {
        //调用无参构造方法
        this();
        System.out.println("有参够方法 = " + person);
    }

    //普通成员方法
    public void getUsername() {
        System.out.println("username method");
    }
    //普通静态方法
    public static void getAge() {
        System.out.println("age method");
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public static void setAge(int age) {
        CompleteClass.age = age;
    }

    public static String getGENDER() {
        return GENDER;
    }

    public String getAddress() {
        return address;
    }

    public static void main(String[] args) {
        //访问类里边的属性,分为两种方式,访问静态的直接使用类名.的形式访问,访问非静态就需要创建对象
        //使用对象的引用去访问。根据java封装的特性,类里边的属性通常都被定义为private的。所以都只能通过
        //方法区访问类里的属性。
        CompleteClass cc = new CompleteClass();
        System.out.println();
        //CompleteClass c = new CompleteClass();
        //程序的执行顺序:①静态代码块②动态代码块③构造方法。并且静态代码块只会被执行一次。

        System.out.println(cc.getAddress());
        cc.getUsername();
        //调用静态方法可以通过类或对象引用
        CompleteClass.getAge();
        cc.getAge();
        //试图修改常量,会报错
        //CompleteClass.GENDER = "女";
        //cc.address = "";
    }
}

八、嵌套类

概念

将一个类定义在另一个类或一个方法内部,称为内部类。

内部类分为:成员内部类、局部内部类、匿名内部类和静态内部类。

类的层次结构

暂时无法在飞书文档外展示此内容

内部类

public class 外部类 {
    public class 内部类 {
    }
}
/**
 * 内部类
 */
public class OuterClass {
    //定义静态属性
    public static String username = "静态属性";
    public String password = "动态属性";
    private int age = 19;
    //构造方法
    public OuterClass() {
        System.out.println("OuterClass 构造方法");
        System.out.println(this);
    }

    //外部类成员方法
    public void show() {
        System.out.println("外部类成员方法");
    }

    public class InnerClass {
        //Inner classes cannot have static declarations
        //public static String username ="";
        public String username = "内部类的username";
        //构造方法
        public InnerClass() {
            System.out.println("InnerClass 构造方法");
            System.out.println(this);//内部类引用
            System.out.println(OuterClass.this);//获取外部类的this引用
        }

        public int sum() {
            //访问外部类的属性
            System.out.println("username = " + username);
            System.out.println("password = " + password);
            System.out.println("age = " + age);
            return 100 + 500;
        }
        //Inner classes cannot have static declarations
        //public static void show() {}
    }
}
public class TestOuterClass {
    public static void main(String[] args) {
        //创建外部类对象
        OuterClass outerClass = new OuterClass();
        //创建内部类对象
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();
        //调用内部类的成员方法
        System.out.println(innerClass.sum());
        //调用外部类成员方法    
        outerClass.show();
        //访问外部类的静态属性,可以通过类名或引用访问
        System.out.println(OuterClass.username);
        System.out.println(outerClass.username);
        //访问外部类的成员属性
        System.out.println(outerClass.password);
        //访问内部类的静态属性,不能实现,因为内部类里不能定义静态属性
        //访问内部类的成员属性
        System.out.println(innerClass.username);
    }
}

静态内部类

public class 外部类 {
    public static class 内部类 {
    }
}
/**
 * 静态内部类可以定义和外部类相同名字的属性。
 * 静态内部类不能够访问外部类里的成员属性
 * 静态内部类不能够访问外部类里的成员方法。
 * 无法在内部类里获取到外部类的this引用
 * 静态内部类也有this引用
 */    
public class OuterClassStatic {
    //静态属性
    public static String username = "outer static username";
    //成员属性
    public String password = "outer password";
    //私有属性
    private static int age = 20;

    public OuterClassStatic() {
        System.out.println("外部类构造方法");
    }

    public void outerM1() {
        System.out.println(age);
    }
    public static class InnerClassStatic {
        //静态属性
        public static String un = "inner static username";
        //成员属性
        public String pwd = "inner password";
        //私有属性
        private static int innerAge = 200;

        public InnerClassStatic() {
            System.out.println("内部类构造方法");
        }
        public void innerM1() {
            //测试是否可以获取外部类的私有属性
            System.out.println(age);
            //访问外部类的属性
            System.out.println(username);
            //不允许访问外部类的成员属性,因为这是一个静态内部类。
            //System.out.println(password);
        }
    }
}
public class TestOuterCLassStatic {
    public static void main(String[] args) {
        //创建外部类对象
        OuterClassStatic ocs = new OuterClassStatic();
        //创建内部类对象
        OuterClassStatic.InnerClassStatic ics = new OuterClassStatic.InnerClassStatic();
        //访问外部类的静态属性和成员属性
        System.out.println(ocs.username);
        System.out.println(OuterClassStatic.username);
        System.out.println(ocs.password);
        //直接访问外部类的私有属性
        //System.out.println(ocs.age);
        //System.out.println(OuterClassStatic.age);
        //访问内部类的属性
        System.out.println(ics.un);
        System.out.println(OuterClassStatic.InnerClassStatic.un);
        System.out.println(ics.pwd);

        //调用内部类的方法访问内部的私有属性
        ics.innerM1();
        //调用外部类的方法访问外部类的私有属性
        ocs.outerM1();
        //测试静态内部类里的成员属性,成员属性属于具体的对象,没创建一个对象就会有一份单独的值。
        OuterClassStatic.InnerClassStatic ics1 = new OuterClassStatic.InnerClassStatic();
        ics1.pwd = "update inner password";
        System.out.println(ics1.pwd);
        System.out.println(ics.pwd);

        //静态属性,不管创建多少个对象,内存中都只会保存一份值
        ics1.un = "update inner static username";
        System.out.println(ics1.un);
        System.out.println(ics.un);
        System.out.println(ics == ics1);
    }
}

匿名内部类

当一个接口中只有一个方法,并且该接口被作为一个方法的参数时,就会使用匿名内部类。

public class AnonymousClass {
    static String username = "helloworld";
    public static void main(String[] args) {
        String s = "abc";
        //定义父类引用指向子类对象
        Father father = new Xiao();
    
        //调用方法。
        sum(father);
        //通过匿名内部类创建子类对象传递给方法的父类引用
        sum(new Father() {
            @Override
            public int m1(int a, int b) {
                System.out.println(s);//只能使用外部变量,但是不能修改,因为当一个变量在匿名内部类中使用时,该变量就会变成final修饰的常量
                username = "hhehe";//全局变量就可以改变。
                System.out.println(username);
                return a * b;
            }
        });
    }
    public static void sum(Father father) {
        int a = 10, b = 220;
        System.out.println(father.m1(a, b));
    }
}
interface Father {
    public int m1(int a, int b);
}
class Xiao implements Father {
    public int m1(int a, int b) {
        System.out.println("m1");
        return a + b;
    }
}

局部内部类

局部内部类: 定义在方法中

特点:

  • 1、成员内部类中只能定义非静态属性和方法
  • 2、成员内部类中可以访问外部类的成员,私有的可以,静态的也可以
  • 3、局部内部类只能在方法内创建对象
  • 局部内部类要访问局部属性,那么该属性必须是被final修饰的局部常量。
  • 在jdk1.7版本中,如果局部变量在局部内部类中使用必须要显式的加上final
  • 在jdk1.8版本中,final是默认加上的
  • 因为局部变量在方法结束后,就会被销毁,而局部内部类的对象却要等到内存回收机制进行销毁,所以如果是常量的话,那么常量就会被存放在常量池中。
public class LocalClass {
    private String un = "outer username";
    private static int age = 20;
    public static void main(String[] args) {
        //访问LocalClass里的成员方法m
        LocalClass localClass = new LocalClass();
        localClass.m();
    }

    public void m() {
        String state = "error";
        System.out.println("m()方法");
        class InnerClassMethod {
            //定义变量
            public String username;
            //定义方法
            public void m() {
                System.out.println(this);
                System.out.println(un);
                System.out.println("age = " + age);
                System.out.println("state = " + state);
                System.out.println("InnerClassMethod m()方法");
            }
        }
        InnerClassMethod icm = new InnerClassMethod();
        icm.m();
        System.out.println(icm.username);
    }
}

随堂案例

设计一个动物声音“模拟器”,希望模拟器可以模拟许多动物的叫声,要求如下:

(1)编写接口Animal, Animal接口有2个抽象方法cry()和getAnimalName(),即要求实现接口的各种具体动物类给出自己的叫声和种类名称。

(2)编写模拟器类Simulator,该类有一个playSound(Animal animal)方法,该方法的参数是Animal类型,即参数animal可以调用实现Animal接口类重写的cry()方法播放具体动物的声音,调用重写的getAnimalName()方法显示动物种类的名称。

(3)编写实现Animal接口的Dog类和Cat类。

(4)编写测试类进行测试,在测试类的main方法中至少包含如下代码。

Simulator simulator = new Simulator();

simulator.playSound(new Dog());

simulator.playSound(new Cat());

public interface Animal {
    public void cry();//叫声
    public String getAnimalName();//获取具体动物的名字
}
public class Dog implements Animal{
    @Override
    public void cry() {
        System.out.println("汪汪汪-------");
    }

    @Override
    public String getAnimalName() {
        return "我是条狗🐶";
    }
}
public class Cat implements Animal{
    @Override
    public void cry() {
        System.out.println("喵喵--------");
    }

    @Override
    public String getAnimalName() {
        return "我是只猫";
    }
}
public class Simulator {
    public void playSound(Animal animal) {
        System.out.println("当前是动物 " + animal.getAnimalName() + " 在叫");
        animal.cry();
    }
}
public class Test {
    public static void main(String[] args) {
        //创建声音模拟器
        Simulator simulator = new Simulator();
        //调用模拟器发声的方法
        simulator.playSound(new Dog());
        simulator.playSound(new Cat());
    }
}

作业:

1、重载一个名为area的方法,然后该名字的方法可以根据传入参数,计算圆形、正方形、三角形、梯形的面积。

public class Homework {
    //定义一个常量
    private static final double PI = 3.14;

    public static void area(double r) {
        System.out.print("圆形的面积:");
        System.out.println(PI * (r * r));
    }
    public static void area(double len, double width, String type) {
        if (type.equals("三角形")){
            System.out.println(String.format("三角形形的面积:%s", len * width/2));
        } else {
            System.out.println(String.format("正方形的面积:%s", len * width));
        }
    }
    public static void area(double upBottom, double downBottom, double high) {
        System.out.println(String.format("正方形的面积:%s", (upBottom + downBottom)*high/2));
    }

    public static void main(String[] args) {
        area(3);
        area(3, 2, "三角形");
        area(3, 3, "正方形");
        area(2, 3, 6);
    }
}

2、某公司的雇员分为以下若干类:

  • I. Employee:这是所有员工总的父类。
  • 1). 属性:员工的姓名,员工的生日月份。
  • 2). 方法:getSalary(int month) 根据参数月份来确定工资,如果该月员工过生日,则公司会额外奖励 100 元。
  • II. SalariedEmployee:Employee 的子类,拿固定工资的员工。
  • 1). 属性:月薪。
  • III. HourlyEmployee:Employee 的子类,按小时拿工资的员工,每月工作超出160小时的部分按照1.5 倍工资发放。
  • ​ 1). 属性:每小时的工资、每月工作的小时数。
  • IV. SalesEmployee:Employee 的子类,销售,工资由月销售额和提成率决定。
  • 1). 属性:月销售额、提成率。
  • V. BasePlusSalesEmployee:SalesEmployee 的子类,有固定底薪的销售人员,工资由底薪加上销售提成部分。
  • 1). 属性:底薪。
  • 要求:
  • I. 创建 SalariedEmployee、HourlyEmployee、SaleEmployee、BasePlusSalesEmployee 四个类的对象各一个。
  • II. 并调用父类 getSalary(int money)方法计算某个月这四个对象各自的工资。
  • 注意:要求把每个类都做成完全封装,不允许非私有化属性。
  • 类图如下:
  • img
public class TestEmployee {
        public static void main(String[] args) {
                Employee[] es = new Employee[4];
                es[0] = new SalariedEmployee("John", 5, 5000);
                es[1] = new HourlyEmployee("Tom", 10, 25, 170);
                es[2] = new SalesEmployee("Lucy", 7, 200000, 0.03);
                es[3] = new BasePlusSalesEmployee("James", 8, 1000000, 0.02, 5000);
                
                for(int i = 0; i<es.length; i++){
                        System.out.println(es[i].getSalary(5));
                }
        }
}
class Employee{
        private String name;
        private int birthMonth;
        public Employee(String name,int birthMonth){
                this.name=name;
                this.birthMonth=birthMonth;
        }
        public String getName(){
                return name;
        }
        public double getSalary(int month){
                if (this.birthMonth==month) return 100;
                else return 0;
        }
}
class SalariedEmployee extends Employee{
        private double salary;
        public SalariedEmployee(String name,int birthMonth,double salary){
                super(name,birthMonth);
                this.salary=salary;
        }
        public double getSalary(int month){
                return salary+super.getSalary(month);
        }
}
class HourlyEmployee extends Employee{
        private double salaryPerHour;
        private int hours;
        public HourlyEmployee(String name, int birthMonth, double salaryPerHour, int hours) {
                super(name, birthMonth);
                this.salaryPerHour = salaryPerHour;
                this.hours = hours;
        }
        public double getSalary(int month){
                double result=0;
                if (hours>160) result=160*this.salaryPerHour+(hours-160)*this.salaryPerHour*1.5;
                else result=this.hours*this.salaryPerHour;
                return result+super.getSalary(month);
        }
}
class SalesEmployee extends Employee{
        private double sales;
        private double rate;
        public SalesEmployee(String name, int birthMonth, double sales, double rate) {
                super(name, birthMonth);
                this.sales = sales;
                this.rate = rate;
        }
        public double getSalary(int month) {
                return this.sales*this.rate+super.getSalary(month);
        }
}
class BasePlusSalesEmployee extends SalesEmployee{
        private double basedSalary;
        public BasePlusSalesEmployee(String name, int birthMonth, double sales, double rate, double basedSalary) {
                super(name, birthMonth, sales, rate);
                this.basedSalary = basedSalary;
        }
        public double getSalary(int month) {
                return this.basedSalary+super.getSalary(month);
        }
}

3、已知一个类 Student 代码如下:

class Student{ 
    String name; 
    int age; 
    String address; 
    String zipCode; 
    String mobile; 
}

I. 把 Student 的属性都作为私有,并提供 get/set 方法以及适当的构造方法。

II. 为 Student 类添加一个 getPostAddress 方法,要求返回 Student 对象的地址和邮编

class Student{
    private String name;
    private int age;
    private String address;
    private String zipCode;
    private String mobile;
    public Student(){}
    public Student(String name, String address, String mobile) {
        this.name = name;
        this.address = address;
        this.mobile = mobile;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getZipCode() {
        return zipCode;
    }

    public void setZipCode(String zipCode) {
        this.zipCode = zipCode;
    }

    public String getMobile() {
        return mobile;
    }

    public void setMobile(String mobile) {
        this.mobile = mobile;
    }

    public String getPostAddress() {
        return "地址 :" + this.address + " 邮编 :" + this.zipCode;
    }
}

九、常用类

Object类

概念

Object 类

  • 1、Object 是所有的类的超类、基类。位于继承树的最顶层。
  • 2、任何一个没有显示定义extends父类的类。都直接继承Object,否则就是间接继承
  • 3、任何一个类都可以享有Object提供的方法
  • 4、Object类可以代表任何一个类(多态),可以作为方法的参数、方法的返回值
  • 5、所有的类,包括数组,都实现了该类里边的方法。

Object中常用方法

getClass方法

此方法用于返回该对象的真实类型(运行时的类型)

  • public final Class<?> getClass()
//判断运行时d对象和c对象是否是同一个类型
Animal d = new Dog();
Animal c = new Cat();

//方式1:通过 instanceof 关键字判断
if((d instanceof Dog && c instanceof Dog) ||(d instanceof Cat && c instanceof Cat)) {
    System.out.println("是同一个类型");
}else {
    System.out.println("不是同一个类型");
}
//方式2:通过getClass方法 判断
if(d.getClass() == c.getClass()) {
    System.out.println("是同一个类型");
}else {
    System.out.println("不是同一个类型");
}

hashCode方法

public native int hashCode();

  • OpenJDK8 默认hashCode的计算方法是通过和当前线程有关的一个随机数+三个确定值,运用Marsaglia's xorshift scheme随机数算法得到的一个随机数。和对象内存地址无关。
  • 不同对象的 hashCode 可能相同;但 hashCode 不同的对象一定不相等
  • 自定义类要求重写该方法
Student stu1 = new Student("zhangsan", 30);
Student stu2 = new Student("zhangsan", 30);
Student stu3 = stu1;
System.out.println(stu1.hashCode());
System.out.println(stu2.hashCode());
System.out.println(stu3.hashCode());

//两个不同的值,hashcode值也有可能相同
String str1 = "通话";
String str2 = "重地";

System.out.println(str1.hashCode());
System.out.println(str2.hashCode());

equals方法

Object类的equals方法的作用是比较两个对象是否相等。比较的是内存地址。其原代码是==

如果想实现内容的比较,那么需要重写equals方法

public class TestObject {
    public static void main(String[] args) {
        //创建两个user对象
        User1 user1 = new User1("abc", 19);
        User1 user2 = new User1("abc", 19);
        //判断两个对象是否相等
        System.out.println(user1 == user2);//false
        //重写equals前返回false,重写equals后返回true
        System.out.println(user1.equals(user2));
        //需求,只要对象内的内容相同,就判断是同一个对象,此时需要重写父类的equals方法
    }
}

class User1 {
    private String username;
    private int age;
    //构造方法

    public User1(String username, int age) {
        this.username = username;
        this.age = age;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public boolean equals(Object obj) {
        //字符串重写了equals方法来比较两个字符串的内容
        return this.age == ((User1)obj).age && this.username.equals(((User1)obj).username);
    }
    @Override
    public int hashCode() {
        //重写hashcode,让各个字段都参与hashcode的计算。
        return Objects.hash(age, username);
    }
}
练习equals重写

有类Employee, 该类包含属性:名字、年龄、地址、性别、学历、年薪。

用代码实现该类,代码满足需求:如果两个Employee对象的名字、年龄、年薪、性别相同,那么两个对象就是同一个对象,否则就不是。

public class TestObject {
    public static void main(String[] args) {
        //创建两个员工对象
        Employee employee1 = new Employee("张三", 25, "abc", 0, "本科", 150000);
        Employee employee2 = new Employee("张三", 25, "abc", 0, "本科", 150000);
        //比较两个员工是不是同一个人,使用等号不能判断两人是否是同一人
        System.out.println(employee1 == employee2);
        //因为要根据员工的名字、年龄、年薪、性别才能判断是否是同一人。所以此时需要重写类Employee的equals方法
        System.out.println(employee1.equals(employee2));

    }
}

//有类Employee, 该类包含属性:名字、年龄、地址、性别、学历、年薪。
class Employee {
    private String name;
    private int age;
    private String address;
    private int gender;
    private String education;
    private double yearSalary;

    public Employee(String name, int age, String address, int gender, String education, double yearSalary) {
        this.name = name;
        this.age = age;
        this.address = address;
        this.gender = gender;
        this.education = education;
        this.yearSalary = yearSalary;
    }
    //省略get和set方法......

    //重写equals方法
    public boolean equals(Object obj) {
        //向下转型
        Employee employee = null;
        if(obj instanceof Employee) {
            employee = (Employee)obj;
        }
        if (employee != null)
            //比较对象里的每一个字段内容
            return this.name.equals(employee.name)
                    && this.age == employee.age
                    && this.yearSalary == employee.yearSalary
                    && this.gender == employee.gender;
        return false;
    }

    //重写hashcode方法
    public int hashcode() {
        return Objects.hash(name, age, yearSalary, gender);
    }
}
重写equals方法
public boolean equals(Object obj) { 
    //1、非空判断
    if(obj == null) {
        return false;
    }
    //2、如果当前对象与obj相等
    if(this == obj) {
        return true;
    }
    //3、判断obj是否属于Student类型
    if(obj instanceof Student) {
        Student stu = (Student)obj;
        //4、判断属性
        if(this.name.equals(stu.name) && this.age == stu.age) {
            return true;
        }
    }
    return false;
}

总结:== 和 equals的区别

  • 两个都是用于比较的
  • == 可以用于基本类型和引用类型
  • equals只能用于引用类型的比较

为什么重写equals方法就要重写hashcode方法?

当我们对比两个对象是否相等时,我们就可以先使用 hashCode 进行比较,如果比较的结果是 true,那么就可以使用 equals 再次确认两个对象是否相等,如果比较的结果是 true,那么这两个对象就是相等的,否则其他情况就认为两个对象不相等。这样就大大的提升了对象比较的效率,这也是为什么 Java 设计使用 hashCode 和 equals 协同的方式,来确认两个对象是否相等的原因。

toString方法

返回对象的字符串表现形式

  • 全限定名+@+十六进制的hash值 commonclasses.TestToString@6e0be858
  • 如果直接输出一个对象,那么默认会调用这个对象的toString方法,而toString方法是Object类提供的,返回的是“对象的地址”。但是我们一般输出对象希望输出的是对象的属性信息,所以可以重写父类的toString方法

如果希望输出对象内部每一个属性的值,就需要我们手动重写Object类的toString方法。

public class TestToString {
    public static void main(String[] args) {
        User user = new User();
        //没有重写toString方法前的打印输出
        //System.out.println(user.toString());//commonclasses.User@fa17fa41

       //重写toString方法后的打印输出
        System.out.println(user.toString());
    }
}

public class User {
    String username = "xiaoming";
    String password = "xiaoming";

    /**
     * 只要两个对象里的内容相同,就判断这两个对象是同一个
     * @param obj
     * @return
     */
    @Override
    public boolean equals(Object obj) {
        //获取具体的对象,通过instanceof
        if (obj instanceof User) {
            User user = (User) obj;
            boolean un = (this.username == user.username);
            boolean pwd = (this.password == user.password);
            return un && pwd;
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(username, password);
    }

    //重写toString方法
    @Override
    public String toString() {
        String res = "username:" + username + "  password:" + password;
        return res;
    }
}

finalize方法

当垃圾回收器回收垃圾对象的时候,自动调用
public class Test5 {
    public static void main(String[] args) {
        Person p = new Person();
        //手动将对象标记为垃圾对象
        p =,098发垃圾回收器,回收垃圾对象
        System.gc();
    }
}
class Person{
    @Override
    protected void finalize() throws Throwable {
        super.finalize();//不要删除
        System.out.println("finalize方法执行了");    
    }
}

注意:finalize()方法会被一个低优先级的线程Finalizer执行,这里所说的“执行”是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。 这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端地发生了死循环

clone克隆

protected native Object clone() throws CloneNotSupportedException;

clode()方法被声明为native的方法,因此,它并不是Java的原生方法,具体的实现是由C/C++完成的。clone英文翻译为"克隆",其目的是创建并返回此对象的一个副本。

创建对象的方式:new,clone

浅拷贝与深拷贝:

浅拷贝

会拷贝对象的常量以及引用。但是引用所指向的另一个对象不会继续拷贝

public class CloneDemo {
    public static void main(String[] args) throws CloneNotSupportedException {
        //创建Car的对象
        Car car1 = new Car("五菱", "白色", 10);
        //使用Object类的clone方法创建car1的一个拷贝
        Car car2 = (Car) car1.clone();
        System.out.println(car1);
        System.out.println(car2);
        //修改克隆后的对象,看是否会改变原生对象
        System.out.println("修改克隆对象后--------");
        car2.setBrand("特斯拉");
        car2.setYear(3);
        System.out.println(car1);
        System.out.println(car2);

        Car car3 = new Car("大众", "黑色",8);
        Person1 xiao = new Person1("小明", 17);
        car3.setPerson(xiao);
        //通过clone复制了一份car3
        Car car4 = (Car) car3.clone();
        System.out.println("car3:" + car3);
        System.out.println("car4:" + car4);
        //修改克隆后的对象
        System.out.println("修改克隆对象后--------");
        Person1 car4Person = car4.getPerson();
        car4Person.setName("小红");
        car4Person.setAge(29);
        System.out.println("car3:" + car3);
        System.out.println("car4:" + car4);
    }
}
/**
 * 如果想让一个类被克隆,那么需要告诉java虚拟机这个类是可以被克隆
 */
class Car implements Cloneable{
    private String brand;
    private String color;
    private int year;
    private Person1 person;

    public Car(String brand, String color, int year) {
        this.brand = brand;
        this.color = color;
        this.year = year;
    }

    //重写父类clone方法
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();//该方法实现浅拷贝,因为对象里的引用类型字段指向的内容没有深入拷贝。
    }

    //为了查看对象里每个属性的值,我们重写toString方法


    @Override
    
    public String toString() {
        return "Car{" +
                "brand='" + brand + '\'' +
                ", color='" + color + '\'' +new
                ", year=" + year +
                ", person=" + person +
                '}';
    }
}

class Person1 implements Cloneable {
    private String name;
    private int age;

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "Person1{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class TestClone implements Cloneable {
    int a = 10;
    String str = new String("clone method");
    public static void main(String[] args) throws CloneNotSupportedException {
        //创建一个TestClone的对象
        TestClone tc = new TestClone();
        //通过克隆创建一个TestClone的副本
        TestClone clone = (TestClone) tc.clone();
        System.out.println(tc.a == clone.a);
        System.out.println(tc.str == clone.str);
    }
}

img

深拷贝

对象里的常量和引用会被拷贝,并且引用指向的对象也会被拷贝。因为java的Object类提供的clone方法提供的拷贝属于浅拷贝,而拷贝引用的同时拷贝引用指向的对象属于深拷贝,此部分逻辑需要用户自动重现clone方法实现。

@Override
protected Object clone() throws CloneNotSupportedException {
    //return super.clone();
    //实现深拷贝
    Car car = (Car) super.clone();//拷贝车子对象
    car.setPerson((Person1) car.getPerson().clone());//车子对象里的人也继续拷贝一份。
    return car;
}

面试题

Object obj = new Object();

这个对象占几个字节:16个字节

img

包装类

思考

img

3.1 概念

为什么要有包装类

  • 因为基本数据类型不具有方法和属性。而引用数据类型可以拥有属性和方法,使用更加的灵活
  • 所以Java给8种基本数据类型提供对应8个包装类。包装类也就是引用数据类型

java中包装类的继承关系

img

基本类型包装类型byteByteshortShortintIntegerlongLongfloatFloatdoubleDoublecharCharacterbooleanBoolean

3.2 装箱和拆箱

img

装箱就是将基本类型转换成包装类

拆箱就是将包装类型转换成基本类型

jdk1.5 之前装箱和拆箱

//在jdk1.5之前   拆装箱的过程
int a = 10;
//装箱
Integer integer = Integer.valueOf(a);
System.out.println(integer);
//拆箱
int b = integer.intValue();
System.out.println(b);
jdk1.5 之后的装箱和拆箱
//在jdk1.5之后   拆装箱的过程        
int i = 10;
//装箱
Integer i1 = i;
System.out.println(i1);
//拆箱
int i2 = i1;
System.out.println(i2);
public static void main(String[] args) {
    byte b = 127;//基本数据类型
    //把基本数据类型的b赋值给引用类型的Byte,这个过程称为自动装箱
    Byte b1 = b;//包装类型,自动装箱
    //把包装类型的b1赋值给基本数据类型b2,这个过程称为自动拆箱
    byte b2 = b1;
    System.out.println(b1.MIN_VALUE);//获取最小值
    System.out.println(b1.MAX_VALUE);//获取最大值
}

3.3 Number类

Byte、Short、Integer、Long、Float、Double六个子类

提供一组方法,用于将其中某一种类型转换成其他类型 xxxValue()方法

Integer a = 100;
Byte b = a.byteValue();
Short c = a.shortValue();
Long d = a.longValue();
Float e = a.floatValue();
Double f = a.doubleValue();
Integer g = a.intValue();

3.4 常用的包装类

Integer 、Double

3.4.1定义方式

//Integer、Double的定义方式
Integer ii1 = new Integer(100);
//或者
Integer ii2 = 100;//自动装箱的过程
Double dd1 = new Double(100.2);
//或者
Double dd2 = 100.2;//自动装箱的过程

3.4.2 常用的属性

System.out.println(Integer.MAX_VALUE);
System.out.println(Integer.MIN_VALUE);
System.out.println(Double.MAX_VALUE);
System.out.println(Double.MIN_VALUE);

3.4.3 常用的方法

将字符串类型的数值转换成int或者是double类型

img

//常用方法:
//将字符串转换成int或者是double
String s = "123";
//方式1:
int i = Integer.parseInt(s);
System.out.println(i);
//方式2:
int i2 = Integer.valueOf(s);
System.out.println(i2);

String s1 = "20.5";
//方式1:
double d = Double.parseDouble(s1);
System.out.println(d);
//方式2:
double d1 = Double.valueOf(s1);
System.out.println(d1);

//java.lang.NumberFormatException   数字格式化异常
String s2 = null;
System.out.println(Integer.parseInt(s2));

3.5 Integer缓冲区(面试题)

public class Demo02 {
    public static void main(String[] args) {
        /**
         * 面试题:整数型包装类缓冲区
         * 整数型的包装类定义缓冲区(-128~127),如果定义的数在这个范围你之内,那么直接从缓存数组中获取,
         * 否则,重新new新的对象
         */
        Integer i1 = new Integer(10);
        Integer i2 = new Integer(10);
        System.out.println(i1 == i2); //false
        System.out.println(i1.equals(i2));//true

        Integer i3 = 1000; //Integer i3 = new  Integer(1000);
        Integer i4 = 1000; //Integer i3 = new  Integer(1000);
        System.out.println(i3 == i4); //false
        System.out.println(i3.equals(i4));//true

        Integer i5 = 100; //IntegerCache.cache[i + (-IntegerCache.low)]
        Integer i6 = 100; //IntegerCache.cache[i + (-IntegerCache.low)]
        System.out.println(i5 == i6);//true
        System.out.println(i5.equals(i6));//true    
    }
}

String类

概念

  • String类代表字符串。 Java程序中的所有字符串文字(例如"abc" )都被实现为此类的实例。
  • String是不可修改的

String创建对象

  1. 直接赋值
  2. 通过构造方法创建对象
//String类的定义
//1、直接赋值
String s = "cxk";
System.out.println(s);
//2、通过构造方法创建String类的对象
String s1 = new String("李四哈哈"); // String s1 = "李四哈哈";
System.out.println(s1);

//通过字节数组变成一个字符串
byte[] b = {97,98,99};
//参数1:字节数组  参数2:起始下标   参数3:长度
String s2 = new String(b, 0, b.length);
System.out.println(s2);

//通过字符数组变成一个字符串
char[] c = {'z','y','x'};
String s3 = new String(c, 0, c.length);
System.out.println(s3);    

String类常用方法

//String str = "abcn12c3fcds";
//charAt(index) 获取指定下标对应的字符,返回char类型
//value['a', 'b', 'c', 'n', '1', '2', 'c', '3', 'f', 'c', 'd', 's']
//System.out.println(str.charAt(3));
//indexOf("字符串")返回此字符串中第一次出现的索引
//System.out.println(str.indexOf("cn1"));

//返回此字符串中指定子字符串最后一次出现的索引
//System.out.println(str.lastIndexOf("b"));

//length()  获取字符串的长度
//System.out.println(str.length());

//判断的方法----------------------------------------------------------------------------
//String str = "abcD";
//判断两个字符串是否相等
//System.out.println("abcd".equals(str));
//判断两个字符串是否相等,忽略大小写
//System.out.println("abcd".equalsIgnoreCase(str));
//判断字符串是否为空串  ""
//System.out.println(str.isEmpty());
//String str = "123478923";
//判断字符串是否以指定的字符串开头
//System.out.println(str.startsWith("12"));
//判断字符串是否以指定的字符串开头,指定开始位置
//System.out.println(str.startsWith("34", 2));
//判断字符串是否以指定的字符串结尾
//System.out.println(str.endsWith("23"));
//判断字符串中是否包含自定的字符串
//System.out.println(str.contains("SB"));

//其他方法----------------------------------------------------------------------------
//     !!! 记得要重新赋值 !!!
//String str = "hello,SB";
//将字符串与指定的字符串进行拼接
//str = str.concat("world"); //str = str + "world";
//System.out.println(str);
//字符串替换:将字符串中指定的字符串替换成指定的字符串
//str = str.replace("SB", "**");
//System.out.println(str);

//字符串截取,从指定的下标开始和结束      范围是左闭右开[0,5)
//str = str.substring(0, 5);
//System.out.println(str);

//字符串截取,从指定的下标开始一直到最后
//str = str.substring(6);
//System.out.println(str);

//字符串切割,按照指定的字符串对原字符串进行切割
//String str = "zhangsanlisiwangwu";
//String[] s = str.split("s");
//System.out.println(Arrays.toString(s));

//去除字符串前后的空格
//String str = "      n你好。。        哈哈      ";
//str = str.trim();
//System.out.println(str);

//String str = "abcd你好";
//将字符串变成字节数组
//byte[] b = str.getBytes();
//System.out.println(Arrays.toString(b));

//将字符串变成字符数组
//char[] c = str.toCharArray();
//System.out.println(Arrays.toString(c));

String str = "abcADC你好";
//将字符串中的字母变成大写
System.out.println(str.toUpperCase());
//将字符串中的字母变成小写
System.out.println(str.toLowerCase());
str.length();

        //int i = 10;
//方式1:
        //String s = i+"";
//方式2:将其他的类型的数据转换成String类型
//String s2 = String.valueOf(i);
//System.out.println(s);
//System.out.println(s2);

拓展:String字符串被创建就不能修改。因为不可变有着非常强大的功能,比如说,缓存、安全性、高性能等

如何理解这个不可变?

String str = "abc";
str = "bcd"
System.out.println(str);
//输出:bcd

作业

1、翻转字符串:String url = "https://www.runoob.com/manual/jdk11api/java.base/java/lang/String.html";

翻转后的结果:String.html/lang/java/java.base/jdk11api/manual/com.runoob.www//:https

public static void main(String[] args) {
    String url = "https://www.runoob.com/manual/jdk11api/java.base/java/lang/String.html";
    //String.html/lang/java/java.base/jdk11api/manual/com.runoob.www//:https
    String[] splits = url.split("://");//按照指定字符分割原字符串
    //把uri按照 / 分割
    String[] uris = splits[1].split("/");
    String result = new String();//最终结果
    //manual/jdk11api/java.base/java/lang/String.html
    for (int  i = uris.length-1; i > 0; i--)
        result = result .concat(uris[i]).concat("/");
    String[] domainNames = uris[0].split("\\.");
    //www.runoob.com
    for (int j = domainNames.length-1; j >=0; j--)
        result = result.concat(domainNames[j]).concat(".");
    //使用substring去掉最终多出的.
    result = result.substring(0, result.length()-1);
    result = result.concat("//:").concat(splits[0]);
    System.out.println(result);
}

2、写一个程序,把你全名的单词首字母打印出来

如:String myName = "Fred F. Connell";

public static void main(String[] args) {
    String myName = "Fred F. Connell";
    //把字符串转成字符数组
    char[] chars = myName.toCharArray();
    for (char ch : chars) {
        //判断字符是否是大写
        if (Character.isUpperCase(ch))
            System.out.print(ch);
    }
}

3、键盘上随机输入一字符串,字符串中包含数字,字母,下划线等各种符号,要求只保留其中的字母。并按字母表升序排序。

public static void main(String[] args) {
    String scanner = "sSFDwe7987sfwewe^&*(sdKwe2232";
    //把字符串转成字符数组
    char[] chars = scanner.toCharArray();
    String result = new String();
    for (char ch : chars) {
        //判断当前字符是否是字母
        if (Character.isLetter(ch))
            result = result.concat(String.valueOf(ch));
    }
    //把字符串中的字母进行排序
    char[] letters = result.toCharArray();
    Arrays.sort(result.toCharArray());
    result = Arrays.toString(letters);
    System.out.println(result);
}

可变字符串

  • StringBuffer
  • StringBuilder

StringBuffer、StringBuilder类

常用方法

  • append(String str);
  • delete(int start, int end)
  • insert(int offset, String str)
  • reverse()
  • toString()
public class StringBufferDemo {
    public static void main(String[] args) {
        //创建StringBuffer对象
        StringBuffer sb = new StringBuffer();

        //常用方法:
        //在字符串的后面追加字符串
        sb.append("abcdef");
        System.out.println(sb); //abcdef

        //删除字符串,从指定的下标开始和结束
        sb.delete(2, 4);
        System.out.println(sb);//abef

        //在指定下标位置添加指定的字符串
        sb.insert(2, "123");
        System.out.println(sb);//ab123ef

        //将字符串翻转
        sb.reverse();
        System.out.println(sb);//fe321ba

        //将StringBuffer转换成String类型
        String s = sb.toString();
        System.out.println(s);
    }
}

String、StringBuffer、StringBuilder区别

img

String、StringBuffer、StringBuilder区别

  • 这三个类都可以用于表示字符串
  • 1、String类是字符串常量类,一旦定义不能改变
  • 2、StringBuffer、StringBuilder是可变的字符串,自带有缓冲区。jdk9之前默认缓冲区大小16个字符,之后是16字节。
  • 3、StringBuffer是线程安全的,所以效率低 StringBuilder是线程不安全的,所以效率高

总结:在大量的字符串拼接的时候,使用 StringBuffer、StringBuilder。而不考虑线程安全的时候,选择StringBuilder,否则选择StringBuffer,StringBuilder和StringBuffer最大只能存放4G的数据。

public class Demo01 {
    public static void main(String[] args) {
        //需求:做100000次字符串拼接

        //获取当前系统时间的毫秒数  4918
//        long start = System.currentTimeMillis();
//        String str = "";
//        for (int i = 0; i < 100000; i++) {
//            str = str + "a";
//        }
//        long end = System.currentTimeMillis();
//        System.out.println("耗时"+(end -start));

        //239
//        long start = System.currentTimeMillis();
//        StringBuffer sb = new StringBuffer();
//        for (int i = 0; i < 10000000; i++) {
//            sb.append("a");
//        }
//        long end = System.currentTimeMillis();
//        System.out.println("耗时"+(end -start));

        //90
        long start = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10000000; i++) {
            sb.append("a");
        }
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end -start));
    }
}

String类面试题

package com.qf.string;
public class StringDemo {
    public static void main(String[] args) {
        String s1 = "ab";
        String s2 = "c";
        String s3 = new String("abc");

        String str1 = "abc";
        String str2 = "abc";
        String str3 = "ab" +"c";
        String str4 = s1 +s2;
        String str5 = new String("abc");
        String str6 = new String("ab")+"c";
        String str7 = s3.intern();
        /**
         * String str1 = "abc";
         * String str2 = "abc";
         * 这个“abc”存放在常量池中,
         * 常量池的特点:
         *    首先会去常量池中找是否有“abc”这个常量字符串,如果有直接用str1指向它,
         *    如果没有将“abc”放到常量池中,用str1指向它
         *    再使用去常量池中找有没有“abc”,这个时候已经有了,直接使用str2指向它。
         */
        System.out.println(str1 == str2);//true
        /**
         * String str3 = "ab" +"c"; 
         * 因为“ab”是常量与“c”拼接之后也会在常量池中。
         */
        System.out.println(str1 == str3);//true
        /**
         * String s1 = "ab";
         * String s2 = "c";
         * 当s1和s2拼接的时候,jdk会将s1和s2转换成StringBuilder类型,然后进行拼接操作,
         * 最终的内容实在堆内存中。
         */
        System.out.println(str1 == str4);//false
        /**
         * String str1 = "abc"; 在常量池
         * String str5 = new String("abc"); 在堆内存
         */
        System.out.println(str1 == str5);//false
        /**
         * String str5 = new String("abc");   在堆内存
         * String str6 = new String("ab")+"c";在堆内存
         */
        System.out.println(str5 == str6);//false
        /**
         * String str1 = "abc"; 在常量池
         * String str6 = new String("ab")+"c"; 在堆内存 
         */
        System.out.println(str1 == str6);//false
        /**
         * String str7 = s3.intern();
         * intern方法的含义:将String类型的对象指向常量池,如果有直接指向,如果没有放一个指向
         */
        System.out.println(str1 == str7);//true
    }
}

intern(),以下程序输出答案是什么?

String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);

String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);

定义: String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。

答案:这段代码在JDK 6中运行,会得到两个false,而在JDK 7中运行,会得到一个true和一个false。产生差异的原因是,在JDK 6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串实例的引用,而由StringBuilder创建的字符串对象实例在Java堆上,所以必然不可能是同一个引用,结果将返回false。

而JDK 7(以及部分其他虚拟机,例如JRockit)的intern()方法实现就不需要再拷贝字符串的实例 到永久代了,既然字符串常量池已经移到Java堆中,那只需要在常量池里记录一下首次出现的实例引用即可因 此 intern ( ) 返 回 的 引 用 和 由 StringBuilder 创 建 的 那个字符串实例就是同 一 个 。 而 对 str2 比 较 返回false ,这 是 因 为 “java”这 个 字 符 串 在 执 行 StringBuilder.toString( ) 之 前 就 已 经 出 现 过 了 , 字符串常量池中已经有它的引用,不符合intern()方法要求“首次遇到”的原则,“计算机软件”这个字符串则是首次出现的,因此结果返回true。

sum.misc.Version类

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

u2是无符号的16位整数,因此理论上允许的的最大长度是2^16=65536。而 java class 文件是使用一种变体UTF-8格式来存放字符的,null 值使用两个 字节来表示,因此只剩下 65536- 2 = 65534个字节。

Random类

jdk提供生成随机数的一个类。

public static void main(String[] args) {
    //创建对象
    Random random1 = new Random();
    //基于一个种子创建一个随机数对象,种子:java会根据传入的种子数,使用一个特殊的算法生产一个伪随机数。
    //如果不传入种子,就会使用系统当前时间作为随机数算法的种子。
    Random random2 = new Random(10000);
    System.out.println(Integer.MIN_VALUE);
    System.out.println(Integer.MAX_VALUE);
    random1.nextInt();//生成最小整数到最大整数之间的一个数。
    int randVal = random1.nextInt(1000);//生成[0, 1000)的随机值
    double v = random1.nextDouble();//生成0.0到1.0但是不包括1.0在内的随机小数。
    System.out.println(v);
}

练习,生成6位随机验证码

public static void main(String[] args) {
    //定义一个数组
    String s = "1234567890qwertyuipasdfghjkzxcvbnmQWERTYUIPASDFGHJKZXCVBNM";
    //创建随机数对象
    Random random = new Random();
    char[] chars = s.toCharArray();
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 6; i++) {
        //通过随机函数获取数组下标。
        sb.append(chars[random.nextInt(58)]);
    }
    System.out.println(sb);
}

//曾昭洋
String codes="1234567890qwertyuipasdfghjkzxcvbnmQWERTYUIPASDFGHJKZXCVBNM";
for (int i = 0; i < 6; i++)  System.out.print(codes.charAt(new Random().nextInt(codes.length())) + "");

日期

Date

表示一个特定时间,精确到毫秒。java.util.Date;
public class DateDemo {
    public static void main(String[] args) {        
        //创建Date类的对象    默认是系统当前时间
//        Date d1 = new Date();
//        //Fri Mar 12 11:35:00 CST 2021 CST可视为美国、澳大利亚、古巴或中国的标准时间。
//        System.out.println(d1);

        /**
         * year:年份,默认从1900开始计算
         * month:月份,默认是0-11
         * date:日期
         */
//        Date d2 = new Date(1998-1900, 2-1, 20);    
//        System.out.println(d2);

//        Date d3 = new Date();
//        //返回当前date日期对应的时间的毫秒数从1970年开始计算的毫秒数
//        System.out.println(d3.getTime()/1000/60/60/24/365);

//        Date d1 = new Date();
//        Date d2 = new Date(1998-1900, 2-1, 20);
//        //判断d1是否在d2之前
//        System.out.println(d1.before(d2));
//        //判断d1是否在d2之后
//        System.out.println(d1.after(d2));

        //随堂案例:计算自己活了多长时间
        Date d1 =  new Date(1998-1900, 2-1, 20);
        Date d2 = new Date();
        long time = d2.getTime() - d1.getTime();
        System.out.println(time/1000/60/60/24);
    }
}

SimpleDateFormat

日期格式化类。java.text.SimpleDateFormat;
package com.qf.date;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SimpleDateFormatDemo {
    public static void main(String[] args) {
//        //创建一个Date对象
//        Date date = new Date();
//        //创建日期格式化对象    2021年03月12日   14:15:30
//        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
//        //调用日期格式化对象的format方法
//        String time = sdf.format(date);
//        System.out.println(time);


//        //将字符串格式的时间转换成Date类型
//        String time = "2021-03-12 14:21:30";
//        //创建日期格式化对象
//        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//        //调用日期格式化对象的parse方法
//        try {
//            Date date = sdf.parse(time);
//            System.out.println(date);
//        } catch (ParseException e) {
//            e.printStackTrace();
//        }
    }
}

Calendar

Calendar 类是一个抽象类,表示一个日历类。其包含有时间的三组方法

  • java.util.Calendar;
  • get(字段) 获取指定字段的值
  • set(字段,值) 设置指定字段的指定值
  • add(字段,值) 在指定的字段添加或者减去指定的值
package com.qf.date;
import java.util.Calendar;
public class CalendarDemo {
    public static void main(String[] args) { 
        //创建日历类对象 
        //Calendar.getInstance();默认表示的系统当前时
        Calendar c = Calendar.getInstance();

        //get方法
        System.out.println(c.get(Calendar.YEAR));
        //月份0-11
        System.out.println(c.get(Calendar.MONTH)+1);
        System.out.println(c.get(Calendar.DATE));
        System.out.println(c.get(Calendar.HOUR_OF_DAY));
        System.out.println(c.get(Calendar.MINUTE));
        System.out.println(c.get(Calendar.SECOND));
        //星期天是1   
        System.out.println(c.get(Calendar.DAY_OF_WEEK));

        System.out.println("====================================================");
        //set方法
        c.set(Calendar.MONTH, 3-1);//为给定日历字段设置值
        System.out.println(c.get(Calendar.MONTH)+1);
        c.set(Calendar.DAY_OF_WEEK, 7);
        System.out.println(c.get(Calendar.DAY_OF_WEEK));

        System.out.println("====================================================");
        //add方法
        c.add(Calendar.MONTH, -4);
        System.out.println(c.get(Calendar.MONTH)+1);
        System.out.println(c.get(Calendar.YEAR));
        
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.DATE, 1);//将日期改为该月1号
        System.out.println("roll后时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(calendar.getTime()));
        calendar.roll(Calendar.DATE, -1);
        System.out.println("roll后时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(calendar.getTime()));
    }
}
综合案例:打印当前月份的日历
public static void main(String[] args) {
        Calendar c1 = Calendar.getInstance();
        System.out.println(c1.get(Calendar.DATE));
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入需要查看日历年份和月份");
        System.out.print("年:");
        int a = scanner.nextInt();
        System.out.print("月:");
        int b = scanner.nextInt();
        //设置当前c1为2022年9月1日
        c1.set(a, b-1, 1);//把月份设置为需要输出月份的1号
        System.out.println("七\t一\t二\t三\t四\t五\t六");
        //获取1日是星期几
        int weekDay = c1.get(Calendar.DAY_OF_WEEK);
        for (int i = 1; i < weekDay; i++) {
            System.out.print("\t");
        }
        //获取当前月份
        for (int i = 1; i <= c1.get(Calendar.DATE); i++) {
            int day = c1.get(Calendar.DATE);
            System.out.print(day + "\t");
            //重新获取星期几
            weekDay = c1.get(Calendar.DAY_OF_WEEK);
            if (weekDay == Calendar.SATURDAY) {
                System.out.println();//如果是星期六就执行换行
            }
            //每次循环天数+1
            c1.add(Calendar.DATE, 1);
        }
    }

JDK8之后新的日期时间API

为什么要引进新的时间日期API?

JDK 1.0中包含了一个java.util.Date类,但是它的大多数方法已经在JDK 1.1引入Calendar类之后被弃用了。而Calendar并不比Date好多少。它们面临的问题是:

  • 可变性:像日期和时间这样的类应该是不可变的。
  • 偏移性:Date中的年份是从1900开始的,而月份都从0开始。
  • 格式化:格式化只对Date有用,Calendar则不行。
  • 此外,它们也不是线程安全的;不能处理闰秒等。

JDK8中引进了新的时间API是java.time,新的 java.time 中包含了所有关于本地日期(LocalDate)本地时间(LocalTime)本地日期时间(LocalDateTime)时区(ZonedDateTime)和持续时间(Duration)的类。历史悠久的 Date 类新增了 toInstant() 方法,用于把 Date 转换成新的表示形式。这些新增的本地化时间日期 API 大大简化了日期时间和本地化的管理。

LocalDate、LocalTime、LocalDateTime类

LocalDate、LocalTime、LocalDateTime 类是其中较重要的几个类,它们的实例是不可变的对象,分别表示使用 ISO-8601日历系统的日期、时间、日期和时间。它们提供了简单的本地日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。

  • java.time.LocalDate; java.time.LocalDateTime; java.time.LocalTime;
  • LocalDate代表IOS格式(yyyy-MM-dd)的日期,可以存储 生日、纪念日等日期。
  • LocalTime表示一个时间,而不是日期。
  • LocalDateTime是用来表示日期和时间的,这是一个最常用的类之一。

示例:

LocalDate localDate = LocalDate.now();// 获取当前的本地日期
LocalTime localTime = LocalTime.now();// 获取当前的本地时间
LocalDateTime localDateTime = LocalDateTime.now();// 获取当前的本地日期和本地时间

System.out.println(localDate);
System.out.println(localTime);
System.out.println(localDateTime);

// of方法:设置年月日时分秒,没有偏移量
LocalDateTime localDateTime1 = LocalDateTime.of(2021, 10, 24, 15, 27, 16);
System.out.println(localDateTime1);

//getXxx()方法 获取属性
System.out.println(localDateTime1.getDayOfMonth());// 24 获取当前对象对应的日期是这个月的第几天
System.out.println(localDateTime1.getDayOfWeek());// SATURDAY 获取当前对象对应的日期是星期几
System.out.println(localDateTime1.getDayOfYear());// 297 获取当前天是这一年的第几天
System.out.println(localDateTime1.getMonth());// OCTOBER 获取月份
System.out.println(localDateTime1.getSecond());// 16 获取秒数

// withXxx() 设置相关的属性(体现不可变性,原先对象的with操作不会改变原来对象的属性)
LocalDateTime localDateTime2 = localDateTime.withHour(4); // 设置小时数为04
System.out.println(localDateTime);
System.out.println(localDateTime2);

// plusXxx() 加操作
LocalDateTime localDateTime3 = localDateTime.plusDays(5); // 在现有时间加上5天
System.out.println(localDateTime);
System.out.println(localDateTime3);

// minusXxx() 减操作
LocalDateTime localDateTime4 = localDateTime.minusMonths(2); // 在现有时间减去2个月
System.out.println(localDateTime.getMonth()); // AUGUST
System.out.println(localDateTime4.getMonth());// JUNE

常用还是LocalDateTime类。

Instant类

Instant类类似于Date类,位于包 java.time.Instant; 概念上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的秒数。因为java.time包是基于纳秒计算的,所以Instant的精度可以达到纳秒级。

示例:

// now():获取本初子午线当地的标准时间
Instant instant = Instant.now();
System.out.println(instant);

// 在UTC时区的基础上加上8个时区(北京时间)
OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8));
System.out.println(offsetDateTime);

// 获取时间戳
System.out.println(instant.toEpochMilli());

// ofEpochMilli():通过给定的时间戳,获取Instant的实例
Instant instant1 = Instant.ofEpochMilli(1659839968542L);
System.out.println(instant1);

DateTimeFormatter类

DateTimeFormatter类类似于SimpleDateFormat类,用于格式化与解析日期或时间。 java.time.format.DateTimeFormatter 类:该类提供了三种格式化方法:

  • 预定义的标准格式。如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
  • 本地化相关的格式。如:ofLocalizedDateTime(FormatStyle.LONG)
  • 自定义的格式。如:ofPattern("yyyy-MM-dd hh:mm:ss"),常用是这种。

示例:

// 实例化方式一 预定义的标准格式。如:`ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME`
        DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
        LocalDateTime localDateTime = LocalDateTime.now();

        // 格式化:将日期转换为字符串,需要传入一个TemporalAccessor类型=的,而LocalDate、LocalTime和LocalDateTime都是
        String str1 = formatter.format(localDateTime);
        System.out.println(localDateTime); // 2022-08-08T22:12:43.555
        System.out.println(str1); // 2022-08-08T22:12:43.555
        // 使用标准格式的格式化出来结果是:2022-08-08T22:12:43.555
        // 解析:解析的字符串也必须是标准格式的字符创
        TemporalAccessor parse = formatter.parse("2020-10-31T14:16:15.801854");
        System.out.println(parse); // {},ISO resolved to 2020-10-31T14:16:15.801854// 实例化方式二:本地化相关的格式
        DateTimeFormatter formatter1 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);// 使用FormatStyle.SHORT进行格式化
        // 格式化
        String str2 = formatter1.format(localDateTime);
        System.out.println(str2); 
        DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
        // 格式化
        String str3 = formatter2.format(LocalDateTime.now());
        System.out.println(str3);
        // 解析
        TemporalAccessor parse1 = formatter2.parse("2019-10-31 02:30:29");
        System.out.println(parse1);

System

系统类

常用方法:

  • System.currentTimeMillis()
  • System.exit(0)
package com.qf.system;
import java.text.SimpleDateFormat;
public class SystemDemo {
    public static void main(String[] args) {
        //System 系统类
        //获取系统当前时间,返回自1970年开始以来时间的毫秒数
        System.out.println(System.currentTimeMillis());

        //格式化时间
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String time = sdf.format(System.currentTimeMillis());
        System.out.println(time);

        //结束当前虚拟机运行   0表示正常退出
        System.exit(0);

        System.out.println("执行吗?");
    }
}

Math

数学计算的工具类,位于java.lang包
package com.qf.math;
public class MathDemo {
    public static void main(String[] args) {
        //求a的b次方法  参数1:底数   参数2:幂数
        System.out.println(Math.pow(2, 10));
        //求a平方根       参数1:要开方的数
        System.out.println(Math.sqrt(100));
        //求a立方根       参数1:要开立方的数
        System.out.println(Math.cbrt(27));

        //向上取整
        System.out.println(Math.ceil(10.2));
        //向下取整
        System.out.println(Math.floor(10.9));
        //四舍五入
        System.out.println(Math.round(10.5));

        //随机数 默认的范围[0,1)
        System.out.println(Math.random());
        //需求:随机一个两位数  [0,1)*90   [0,90) + 10     
        System.out.println((int)(Math.random()*90)+10);
    }
}

BigDecimal

为什么使用BigDecimal?

java.math.BigDecimal;

以下的代码的错误原因是0.9在计算中中的二进制是一个无限循环的

double保存时近似值,所以计算的结果不精确

double d1 = 1.0;

double d2 = 0.9;

System.out.println(d1-d2);//0.0999999998

BigDeicmal基本用法

  • 位置:java.math包中。
  • 作用:精确计算浮点数。
  • 创建方式:BigDecimal bd=new BigDecimal(“1.0”)。

常用方法:

方法名描述BigDecimal add(BigDecimal bd)加BigDecimal subtract(BigDecimal bd)减BigDecimal multiply(BigDecimal bd)乘BigDecimal divide(BigDecimal bd)除

package com.qf.math;
import java.math.BigDecimal;
import java.math.MathContext;
public class BigDecimalDemo {
    public static void main(String[] args) {
        double d1 = 1.0;
        double d2 = 0.9;
        //0.09999999999999998
        System.out.println(d1-d2);        
        //使用BigDecimal类解决
        //创建BigDecimal对象
        BigDecimal bd1 = new BigDecimal("1.012097");
        BigDecimal bd2 = new BigDecimal("0.22");
        System.out.println(bd1.add(bd2));
        System.out.println(bd1.subtract(bd2));
        System.out.println(bd1.multiply(bd2));
        //ArithmeticException 算术异常
        /**
         * 参数说明:
         *     参数1:被除数
         *     参数2:保留小数位数
         *  参数3:舍入模式
         *          ROUND_CEILING 向上取整
         *          ROUND_FLOOR   向下取整
         *          ROUND_HALF_UP 四舍五入
         */    
        System.out.println(bd1.divide(bd2,2,BigDecimal.ROUND_HALF_UP));
    }
}

枚举

java中使用关键字来定义枚举

语法格式:enum 名字{VALUE1, VALUE2, VALUE3;}

public class EnumDemo {
    public static void main(String[] args) {
        System.out.println(WeekDay.FRIDAY);//使用自定义类实现枚举,然后获取类里的值
        //获取枚举变量的名字
        System.out.println(WeekDays.MONDAY);
        //获取枚举常量的具体值
        System.out.println(WeekDays.MONDAY.getKey());
        System.out.println(WeekDays.MONDAY.getValue());
        System.out.println(WeekDays.FRIDAY.toString());
    }
}

//定义一个类表示周一到周日,该类只能表示1到7,其他天数不允许
//用类实现
class WeekDay {
    public static final WeekDay MONDAY = new WeekDay("星期一", "MONDAY");
    public static final WeekDay TUESDAY = new WeekDay("星期一", "TUESDAY");
    public static final WeekDay WEDNESDAY = new WeekDay("星期一", "WEDNESDAY");
    public static final WeekDay THURSDAY = new WeekDay("星期一", "THURSDAY");
    public static final WeekDay FRIDAY = new WeekDay("星期一", "FRIDAY");
    public static final WeekDay SATURDAY = new WeekDay("星期一", "SATURDAY");
    public static final WeekDay SUNDAY = new WeekDay("星期一", "SUNDAY");
    private String cnWeek;
    private String enWeek;

    private WeekDay(String cnWeek, String enWeek) {
        this.cnWeek = cnWeek;
        this.enWeek = enWeek;
    }

    public String getCnWeek() {
        return cnWeek;
    }

    public void setCnWeek(String cnWeek) {
        this.cnWeek = cnWeek;
    }

    public String getEnWeek() {
        return enWeek;
    }

    public void setEnWeek(String enWeek) {
        this.enWeek = enWeek;
    }
}
//用枚举实现
enum  WeekDays {
    /*public static final WeekDays MONDAY = new WeekDays("星期一", "MONDAY");
    public static final WeekDays TUESDAY = new WeekDays("星期一", "TUESDAY");
    public static final WeekDays WEDNESDAY = new WeekDays("星期一", "WEDNESDAY");
    public static final WeekDays THURSDAY = new WeekDays("星期一", "THURSDAY");
    public static final WeekDays FRIDAY = new WeekDays("星期一", "FRIDAY");
    public static final WeekDays SATURDAY = new WeekDays("星期一", "SATURDAY");
    public static final WeekDays SUNDAY = new WeekDays("星期一", "SUNDAY");*/
    MONDAY("星期一", "MONDAY"),
    TUESDAY("星期二", "TUESDAY"),
    WEDNESDAY("星期三", "WEDNESDAY"),
    THURSDAY("星期四", "THURSDAY"),
    FRIDAY("星期五", "FRIDAY"),
    SATURDAY("星期六", "SATURDAY"),
    SUNDAY("星期七", "SUNDAY");
    private String cnWeek;
    private String enWeek;
    WeekDays(String cnWeek, String enWeek) {
        this.cnWeek = cnWeek;
        this.enWeek = enWeek;
    }

    public String getKey() {
        return cnWeek;
    }

    public String getValue() {
        return enWeek;
    }

    @Override
    public String toString() {
        return "WeekDays{" +
                "cnWeek='" + cnWeek + '\'' +
                ", enWeek='" + enWeek + '\'' +
                '}';
    }
}

作业

一、必做题

1、编程题

a、完成一个抽查作业程序,定义一个字符串数组保存姓名,一次随机3个不重复的姓名并输出

public static void one() {
    //定义一个字符串数组保存姓名,一次随机3个不重复的姓名并输出
    Random random = new Random();
    StringBuilder str = new StringBuilder();
    //记录获取到的名字个数
    int count = 0;
    while (count < 3) {
        //随机获取数组下标
        int index = random.nextInt(names.length);
        if (!str.toString().contains(names[index])) {
            str.append(names[index]).append("  ");
            count++;
        }
    }
    System.out.println(str);
}


//曾昭洋
String[] s = {"张三","李四","王五","赵六","钱七"};
ArrayList<String> arrayList = new ArrayList<>();//装三个随机名字

for (int i = 0; i < 3; i++) {
    int random = new  Random().nextInt(s.length);//下标
    if (arrayList.contains(s[random])) i--;// 如果已经存在名字,本次不算
    else arrayList.add(s[random]);//没存在就放入
}
for (String str : arrayList) System.out.print(str+" ");//遍历

​ b、用两种方法将系统当前的时间并格式化成(2019-11-12 12 :30: 21)这种格式

public static void two() {
    System.out.println(new SimpleDateFormat("y-M-d HH:mm:ss").format(new Date()));
    System.out.println(DateTimeFormatter.ofPattern("y-M-d HH:mm:ss").format(LocalDateTime.now()));
}

​ c、定义一个方法,可以计算出两个日期之间相差多少天例如:

​ 2018年2月17日到2019年3月29日中间一共有多少天?

public static void three() {
    //2018年2月17日到2019年3月29日中间一共有多少天?
    long one = LocalDate.of(2018, 2, 17).toEpochDay();
    long two = LocalDate.of(2019, 2, 18).toEpochDay();
    System.out.println(two - one);
}

//曾昭洋
//long 相差时间 = DAYS.between(开始时间,结束时间) 
System.out.println(DAYS.between(LocalDate.of(2018,2,17), LocalDate.of(2019,3,29)));

2、完成一个猜拳的小游戏。

​ 由系统随机一个数(0表示石头1表示剪刀2表示布),然后用户输入一个数(0表示石头1表示剪刀2表示布)。输出得出谁赢、谁输,还是平局。

​ 输出的时候要输出“石头”、“剪刀”、“布”,不要输出0、1、2

​ 可以写在一个循环里面。输入y表示还想玩,n表示退出游戏。结束之后要输出赢了多少次、输了多少次、平局多少次、记录游戏时长

public static void four() {
    //获取游戏开始时间
    long start = System.currentTimeMillis();
    //构建二维数组枚举出用户胜负和平局
    String[][] winOrLose = {{"平局", "用户胜", "用户败"},
                            {"用户败", "平局", "用户胜"},
                            {"用户胜", "用户败", "平局"}};
    int[] os = {0, 1, 2};
    String[] userGuess = {"石头", "剪刀", "布"};
    String[] osGuess = {"石头", "剪刀", "布"};
    //定义变量分别记录平局次数,用户胜和败多少次
    int draw = 0, userWin = 0, userLose = 0;
    //定义随机函数
    Random random = new Random();
    //创建键盘输入对象
    Scanner scanner = new Scanner(System.in);
    System.out.println("开猜拳游戏。。。。。。。。。。");
    //猜拳游戏
    do {
        System.out.println("请出拳:0[石头]    1[剪刀]    2[布]");
        int userInput = scanner.nextInt();//用户出拳
        int osInput = os[random.nextInt(os.length)];//系统出拳
        //根据系统和用户出拳作为二维数组下标去二维数组里获取结果
        String result = winOrLose[userInput][osInput];
        if (result.equals("平局")) draw++;
        else if (result.equals("用户胜")) userWin++;
        else if (result.equals("用户败")) userLose++;
        System.out.println("本次游戏结束");
        System.out.println("用户出: " + userGuess[userInput] + "  系统出 : " + osGuess[osInput]);
        System.out.println("输入[y]继续游戏,输入[n]退出游戏");
        String val = scanner.next();
        if (val.equals("n"))break;
    }while (true);
    System.out.println("本次游戏浪费了你 " + (System.currentTimeMillis() - start)/1000 + " 秒");
    System.out.println("平局:" + draw);
    System.out.println("用户胜:" + userWin);
    System.out.println("用户败:" + userLose);
    System.out.println("系统胜:" + userLose);
    System.out.println("系统败:" + userWin);
}

3、使用枚举定义一个类表示一月到十二月

enum  Month {
    //January(一月)、February(二月)、March(三月)、April(四月)、
    // May(五月)、June(六月)、July(七月)、August(八月)、September(九月)、
    // October(十月)、November(十一月)、December(十二月)
    JAN("一月", "Jan"), FEB("二月", "Feb"), MAR("三月", "Mar"),
    APR("四月", "Apr"), MAY("五月", "May"), JUN("六月", "Jun"),
    JUL("七月", "Jul"), AUG("八月", "Aug"), SEP("九月", "Sep"),
    OCT("十月", "Oct"), NOV("十一月", "Nov"), DEC("十二月", "Dec");
    private String key;
    private String value;
    Month(String key, String value) {
        this.key = key;
        this.value = value;
    }
    public String getKey() {
        return key;
    }

    public String getValue() {
        return value;
    }
}

二、选做题

输入年份和月份,打印日历

输入年份,打印一年的日历

十、集合

1、介绍

1.1 概念

集合也叫容器,是用来存放其他对象的对象。所以容器本身也是一个对象。

1.2 集合架构

img

1.3 为什么使用 Java 集合

  • 降低编程难度
  • 提高程序性能
  • 提高API间的互操作性
  • 降低学习难度
  • 降低设计和实现相关API的难度
  • 增加程序的重用性

注意:java容器里只能存放对象,对于基本数据,会自动拆箱和装箱,虽然自动装拆箱会导致性能和空间的开销,但是这种设计方式可以简化开发人员的编码。

1.4 体系结构

容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对映射表。主要包含如下内容:

  1. List

    1. ArrayList:基于动态数组实现,支持随机访问。
    2. Vector:和 ArrayList 类似,但它是线程安全的,但在jdk1.5后被CopyOnWriteArrayList代替。
    3. LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
  2. Set

    1. TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
    2. HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
    3. LinkedHashSet:具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。
  3. Map

    1. TreeMap:基于红黑树实现。
    2. HashMap:基于哈希表实现。
    3. HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
    4. LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
  4. Queue

    1. LinkedList:可以用它来实现双向队列。
    2. PriorityQueue:基于堆结构实现,可以用它来实现优先队列。

2、Collection 接口

2.1、常用方法

  • add()方法向集合中添加元素
  • clear()方法,清空集合中所有元素
  • contains()方法 判断集合是否包含某个元素
  • isEmpty判断集合是否为空
  • remove方法 移除集合中元素,返回boolean类型。如果集合中不包含次元素,则删除失败
  • size()返回集合中元素的个数
  • toArray将集合转换成数组。
  • addAll 向一个集合中添加另一个集合
  • containsAll 判断一个集合中是否包含另一个集合
  • removeAll 从一个集合中移除另一个集合

3、 迭代器

3.1 Iterator

迭代器用于遍历列表和修改元素。迭代器接口具有以下三种方法:

  1. public boolean hasNext() – 如果迭代器有更多元素,此方法返回 true。
  2. public object next() - 它返回元素并将光标指针移动到下一个元素。
  3. public void remove() - 此方法删除迭代器返回的最后一个元素。

3.2 迭代器原理

迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值。

3.3.2 迭代器使用常见问题

  • 1、迭代器迭代完成之后,迭代器的位置在最后一位。 所以迭代器只能迭代一次
  • 2、迭代器在迭代的时候,不要调用多次next方法,可能会出错 NoSuchElementException
  • 3、在迭代器迭代的时候,不能向集合中添加或者删除元素 ConcurrentModificationException

4、泛型基本使用

泛型:参数化类型 JDK1.5之后

  • 用途:
  • 泛型擦除: JDK1.7之后
  • 泛型需要注意的问题:

5、List 接口

List是可能包含重复元素的有序集合。它是一个扩展 Collection 接口的接口。List进一步分为以下几类:

  1. ArrayList
  2. LinkedList
  3. Vectors

5.1、ArrayList类

ArrayList 是 List 接口的实现,可以在List中动态添加或删除元素。此外,如果添加的元素超过初始大小,则ArrayList的大小会动态增加。

ArrayList注意点:默认构造参数不会初始化容器大小,但是可以调用带参的构造方法给他一个初始容量大小 关于arraylist扩容:扩容机制 扩容后的大小=老数组大小+(老数组大小/2) 调用默认构造方法创建arrylist时不会初始化容器大小, 需要等到我们真正在添加值的时候才会给容器分配大小,默认分配为10

常用方法:

  • add(int index, E element)
  • remove(int index)
  • set(int index, E element)
  • get(int index)
  • subList(int beginIndex,int endIndex)
  • list.listIterator();

随堂练习

/**
 * ArrayList的使用: 它是一种可以存放重复元素,并且按照存储顺序排序的一种数据结构.
 */
public class ArrayListDemo {
    public static void main(String[] args) {
        //调用ArrayList的无参构造方法创建一个对象
        ArrayList arrayList = new ArrayList();
        //使用容器的引用调用方法存储数据
        arrayList.add(1);//容器内部会自动把1包装成对象类存储
        arrayList.add(new Person());//存储对象
        arrayList.add("helloworld");//存储字符串
        arrayList.add(true);//存储布尔类型
        arrayList.add(m());//可以在add实参里调用方法
        arrayList.add(3.14);//存储浮点数
        //获取集合中0位置的元素
        System.out.println(arrayList.get(0));
        //修改集合中3位置的数据
        System.out.println(arrayList.get(3));
        arrayList.set(3,true);//会覆盖掉原来该位置的值
        System.out.println(arrayList.get(3));
        //删除指定内容的元数
        arrayList.remove("helloworld");
        //根据位置删除元素
        arrayList.remove(1);
        arrayList.add(3, "Ten");//在指定位置插入指定值,原来该位置的值会向后移动
        //返回某个元素在集合中的位置
        System.out.println(arrayList.indexOf(3.14));
        //使用迭代器iterator遍历集合。
        Iterator iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            //获取当前游标指向的值
            Object value = iterator.next();
            System.out.print(value + " ");
        }
        System.out.println();
        //使用游标遍历相对复杂,因此我们可以使用游标的一个语法糖来遍历,也即是增强for循环
        for (Object obj : arrayList)
            System.out.print(obj + " ");
        System.out.println();
        //使用普通for循环遍历集合
        for (int i = 0; i < arrayList.size(); i++)
            System.out.print(arrayList.get(i) + "  ");
        System.out.println();

        //实际开发中,一种集合通常只能存储同一种数据类型。此时就需要给集合加上泛型限制
        //定义一个集合只能存储字符串类型 Arraylist<数据类型> 引用名 = new ArrayList();
        ArrayList<String> names = new ArrayList<>();
        names.add("张三");
        names.add("李四");
        names.add("王五");
        //存储整数
        //names.add(0);报错,因为泛型已经限制该集合只能存储字符串
        //定义一个集合只能存储User1这种对象
        ArrayList<User1> users = new ArrayList<>();//该集合只能存User1的对象或者是User1的子类对象
        users.add(new User1("张三"));
        users.add(new User1("王五"));
        users.add(new User2("赵强"));
        //使用增强for循环遍历集合users
        for (User1 user : users)
            System.out.println(user);//如果此时想查看对象内部具体的值,那么此对象需要提供toString方法
    }

    public static String m() {
        return "你好";
    }
}

class User1 {
    private String name;

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User1{" +
                "name='" + name + '\'' +
                '}';
    }
}
class User2 extends User1{
    public User2(String name) {
        super(name);
    }
}

5.1.1 实现原理

ArrayList其底层实现使用数组

img

5.2、LinkedList类

LinkedList是包含项目的链接序列。每个链接都包含与另一个链接的连接。

5.2.1 常用方法

常用的方法与ArrayList一致。自己独有一些向首尾添加移除等方法(可以模拟对列、堆栈等数据结构)
package com.qf.demo02;

import java.util.LinkedList;

public class LinkedListDemo {
    public static void main(String[] args) {
        //创建LinkedList对象
        LinkedList<String> list = new LinkedList<String>();
        list.add("jack");
        list.add("rose");
        list.add("cxk");
        list.add("李雷");
        list.add("韩梅梅");
        list.add(1, "马冬梅");
        System.out.println(list);

        list.addFirst("尼古拉斯");
        list.addLast("亚洲舞王");
        System.out.println(list);

        System.out.println("================================================");
        System.out.println(list.getFirst());
        System.out.println(list.getLast());
        System.out.println(list.get(3));


        System.out.println("================模拟栈结构==================");
        LinkedList<String> list1 = new LinkedList<String>();
        list1.push("aa");
        list1.push("bb");
        System.out.println(list1.pop());
        System.out.println(list1.pop());

        System.out.println("================模拟对列结构==================");
        LinkedList<String> list2 = new LinkedList<String>();
        //向对列的尾部添加元素
        list2.offer("哈哈");
        list2.offer("呵呵");
        list2.offer("嘻嘻");
        list2.offer("hiahia");
        //获取并移除对列的头部的元素
        System.out.println(list2.poll());
        System.out.println(list2.poll());
        System.out.println(list2.poll());
        System.out.println(list2.poll());
        //获取但不移除对列的头部的元素
        //System.out.println(list2.element());
        //获取但不移除对列的头部的元素
        //System.out.println(list2.peek());
        System.out.println(list2);

        //LinkedList集合三种遍历方式
    }
}

5.2.3 LinkedList实现原理

Java LinkedList 类使用两种类型的链表来存储元素:

  • 单链表
  • 双向链表

单链表:在单链表中,此列表中的每个节点都存储节点的数据以及指向列表中下一个节点的指针或引用。

img

双链表:在双链表中,它有两个引用,一个指向下一个节点,另一个指向前一个节点。

img

5.3、Vector类

5.3.1 常用方法

ArrayList的方法基本一致
public class VectorDemo {
    public static void main(String[] args) {
        Vector<String> vector = new Vector<String>();
        vector.add("aa");
        System.out.println(vector);
        /**
         * ArrayList、LinkedList、Vector的区别
         * 
         * ArrayList和Vector底层使用数组实现(增删慢、查询快)
         *   ArrayList是当添加元素的时候,才会扩容数组,默认长度为10
         *   Vector是当创建对象的是,就创建长度为10的数组
         *   ArrayList线程不安全,效率高
         *   Vector是线程安全的,效率低
         *   
         * LinkedList底层使用双向链表实现(增删快、查询慢)
         */
    } 
}

5.3.2 实现原理

ArrayList的底层一致,使用数组实现

img

回顾:

6、Map集合

6.1 Map集合特点

  • Map集合是双列集合,由key和value组成。称之为键值对
  • 键的特点:无序,无下标,不重复。
  • 值的特点:无序,无下标,可重复

6.2 Map集合体系结构

暂时无法在飞书文档外展示此内容

6.3 HashMap

中文叫哈希表或散列表

img

6.3.1 HashMap基本使用

常用方法

  • put(K key, V value)
  • get(Object key)
  • Set keySet()
  • Collection values()
  • Set<Map.Entry<K,V>> entrySet()
  • boolean containsKey(Object key)
  • boolean containsValue(Object value)
  • V remove(Object key)
  • int size()
package com.qf.demo01;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Set;
public class HashMapDemo {
    public static void main(String[] args) {
        //创建HashMap
        HashMap<String, String> map = new HashMap<String, String>(12);
        //向map集合中添加元素
        map.put("usa", "漂亮国");
        map.put("jp", "日本");
        map.put("en", "英国");
        map.put("ca", "加拿大");
        map.put("cn", "中华人民共和国");
        map.put("cn", "中国");
        map.put("china", "中国");
        System.out.println(map);
        //从集合中获取元素的值。    根据key获取对应value
        System.out.println(map.get("cn"));
        System.out.println(map.get("usa"));

        //清空map集合中的元素
        //map.clear();
        //System.out.println(map);
        //判断是否包含指定的key
        System.out.println(map.containsKey("cxk"));
        //判断是否包含指定的value
        System.out.println(map.containsValue("中国"));
        //判断集合中的元素长度是否为0
        System.out.println(map.isEmpty());
        //根据key移除map中的元素
        map.remove("jp");
        System.out.println(map);
        //返回map集合中元素的个数
        System.out.println(map.size());        
        System.out.println("=================================");
        //返回map集合中所有的key
        Set<String> keySet = map.keySet();
        for (String key : keySet) {
            System.out.println(key);
        }
        System.out.println("=================================");
        //返回map集合中所有的value
        Collection<String> values = map.values();
        for (String value : values) {
            System.out.println(value);
        }
        System.out.println("=================================");
        //返回map集合中所有的key和value (Entry)
        

        for (Entry<String, String> entry : entrySet) {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }
        System.out.println("=================================");   
        
        //遍历
        for (Object entry: hashMap.entrySet()) {
            Map.Entry en = (Map.Entry) entry;
            System.out.print("<" + en.getKey() + ", ");
            System.out.print(en.getValue() + ">\n");
        } 
        
        //hashmap泛型基本使用
        //如果希望hashmap存储指定类型的键或值,那么可以通过泛型来限制HashMap<数据类型, 数据类型>
        //尖括号内的数据类型不能是基本数据类型,必须都是引用类型
        HashMap<String, Integer> hash1 = new HashMap<>();
        hash1.put("张三", 18);
        //hash1.put("张三", "");不允许插入泛型限制之外的数据类型
        hash1.put("李四", 28);
        HashMap<Object, Object> hash2 = new HashMap<>();
        hash2.put(1, "1");
        hash2.put(2, 3);
        HashMap<String, User3> hash3 = new HashMap<>();//value存放自定义对象类型
        hash3.put("user1", new User3("user3"));
        hash3.put("user2", new User4());//可以存放泛型限制的子类
        //hash3.put("user3", new Object());//不能存放父类
        
        //如何访问hashmap中的值
        System.out.println(hash1.get("张三"));//根据key获取value
        //注意:不能根据value获取key
        System.out.println(hash1.containsKey("李四"));//集合中是否包含指定key
        System.out.println(hash1.containsValue(18));//集合中是否包含指定value
    }
}

Hash函数

LINEAR PROBE HASHING : 线性探测hash

img

6.3.2 HashMap实际应用

  • 可以使用Map 表示一个实体类
  • 可以使用List<Map<String,Object>> 表示一个实体类集合
public class HashMapDemo02 {
    /**
     * 表示一件商品:商品编号、名称、价格、产地、上架时间....
     * 
     * 实体类表示:
     *  一件商品:Product对象
     *      public class Product{
     *          private int id;
     *          private String name;
     *          private double price;
     *          .....
     *      }
     *      Product product = new Product(1,"手机",3000...);
     *      
     *  多件商品:List<Product>
     * 
     * Map表示:
     *     一件商品:Map对象
     *         Map<String,Object> map = new HashMap<>();
     *      map.put("id",1);
     *      map.put("name","电脑");
     *      map.put("price",3000.5);
     * 
     *  多件商品:List<Map<String,Object>>
     */
    public static void main(String[] args) {
        //使用Map表示一件商品
        Map<String, Object> map1 = new HashMap<String, Object>();
        map1.put("id", 1);
        map1.put("name", "电脑");
        map1.put("price",3000.5);
        map1.put("createDate", new Date());
        System.out.println("map1 : " + map1);
        
        Map<String, Object> map2 = new HashMap<String, Object>();
        map2.put("id", 2);
        map2.put("name", "电脑2");
        map2.put("price",3002.5);
        map2.put("createDate", new Date());
        System.out.println("map2 : " + map2);
        
        //使用List<Map>表示多件商品
        List<Map<String, Object>> list = new ArrayList<Map<String,Object>>();
        list.add(map1);
        list.add(map2);
        
        for (Map<String, Object> map3 : list) {
            System.out.println(map3);
            for (Map.Entry<String, Object> entry : map3.entrySet()) {
                System.out.println(entry.getKey() + "==" + entry.getValue());
            }
        }
        //使用集合实现存储省市信息
    }
}

6.3.4 HashMap练习

案例:使用集合保存省市数据
public static void main(String[] args) {
    Map<String, List<String>> map = new HashMap<String, List<String>>();
    List<String> city1 = new ArrayList<String>();
    city1.add("武汉");
    city1.add("监利");
    city1.add("黄冈");
    city1.add("荆州");
    map.put("湖北省", city1);
    List<String> city2 = new ArrayList<String>();
    city2.add("长沙");
    city2.add("岳阳");
    city2.add("常德");
    city2.add("湘潭");
    map.put("湖南省", city2);
    List<String> city3 = new ArrayList<>();
    city3.add("贵阳");
    city3.add("六盘水");
    city3.add("遵义");
    city3.add("铜仁");
    city3.add("兴义");
    city3.add("毕节");
    city3.add("安顺");
    city3.add("凯里");
    city3.add("都匀");
    map.put("贵州省", city3);
    System.out.println(map);
    System.out.println(map.get("湖北省"));
    //遍历贵州各市
    List<String> guizhou = map.get("贵州省");//根据key获取value
    for (String city : guizhou)
        System.out.print(city + "  ");
    System.out.println();
    //遍历hasmap
    for (Map.Entry<String, List<String>> entry : map.entrySet()) {
        System.out.print(entry.getKey() + " : ");
        //遍历各个市
        for (String c : entry.getValue())
            System.out.print(c + "  ");
        System.out.println();
    }
}

6.4 HashMap源码解析

img

6.4.1 put的过程原码

img

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //tab表示存放Node节点的数据   p表示当前节点   n表示长度  i表示节点在数组中的下标               
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判断数组如果为空或者数组长度为0,那么就对数组进行扩容,数组默认初始大小为16
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //将数组的长度-1与hash值进行与运算(计算的结果一定是0~数组长度-1)得到元素应该存放的下标
    //如果当前下标位置为空,那么直接将Node节点存放在当前位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    //如果当前位置不为空(分为三种情况)
    else {
        Node<K,V> e; K k;
        //情况1:要添加的元素与当前位置上的元素相同(hash(hashCode)、key(equals)一致),则直接替换
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //情况2:如果要添加的元素是红黑树节点,那么将其添加到红黑树上
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //情况3:如果要添加的元素是链表,则需要遍历
        else {
            for (int binCount = 0; ; ++binCount) {
                //将当前元素的下一个节点赋给e
                //如果e为空,则创建新的元素节点放在当前位置的下一个元素上,并退出循环
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //如果链表的元素个数大于8个(且当数组中的元素个数大于64),则将其转换成红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //要添加的元素与当前位置上的元素相同(hash(hashCode)、key(equals)一致),则直接退出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //如果返回的e不为null
        if (e != null) { // existing mapping for key
            //将e的值赋给oldValue
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            //返回以前的值(当添加的元素已经存在返回的是以前的值)
            return oldValue;
        }
    }
    ++modCount;
    //如果数组的元素个数大于阈值则进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

6.4.2 resize过程原码

img

final Node<K,V>[] resize() {
    //oldTab 表示原来数组(如果是第二次扩容:长度为16的那个)
    Node<K,V>[] oldTab = table;
    //oldCap 表示原数组的容量(长度)
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //oldThr 表示数组原来的阈值 12
    int oldThr = threshold;
    //newCap 新数组的容量 newThr 新数组的阈值
    int newCap, newThr = 0;

    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //新数组的容量扩大一半  newCap 32
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //新阈值扩大老阈值的一半  newThr 24
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //threshold 24
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //创建一个长度为32的数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //table指向新数组
    table = newTab;
    if (oldTab != null) {
        //将原数组中的元素拷贝到新数组中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //如果当前位置元素不为空
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                //情况1:当前位置上的下一个元素为空,则直接将这个元素拷贝到新数组中
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;    
                //情况2:当前位置上的元素红黑树类型,则需要进行切割
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //情况3:当前位置上的元素链表类型,则需要进行分散拷贝
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

6.4.3 get的过程原码

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //当前first与要找到的hash和key都相等直接返回当前这个first元素
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //如果当前first不为空(有两种情况)
        if ((e = first.next) != null) {
            //当前位置是一个红黑树
            if (first instanceof TreeNode)
                //根据hash、key从红黑树上找到对应的元素
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //当前位置是一个链表
            do {
                //循环进行比较直到找到向的hash和key的元素,并返回
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    //如果数组的为空、数组的长度为0、当前下标位置上的值为null,这三种情况都返回null
    return null;
}

6.5 HashTable

Hashtable常用方法与HashMap一致

HashMap与Hashtable区别:

  • 1、Hashtable是线程安全的,HashMap是线程不安全的
  • 2、Hashtable中不允许存储null作为key和value,而HashMap可以

在实际开发中一般都是用HashMap。考虑线程安全使用ConCurrentHashMap

public static void main(String[] args) {
    Hashtable<Integer, String> hashtable = new Hashtable<>();
    hashtable.put(2, "b");
    hashtable.put(1, "a");
    hashtable.put(5, "l");
    hashtable.put(9, "h");
    hashtable.put(20, "hz");
    //hashtable.put(10, null);//hashtable不允许插入空键或者是值,会报NullPointerException
    hashtable.remove(10);
    System.out.println(hashtable);
    //遍历
    for (Map.Entry<Integer, String> entry : hashtable.entrySet()) {
        System.out.print("<" + entry.getKey() + ", ");
        System.out.print(entry.getValue() + ">     ");
    }
}

6.6 TreeMap

根据给定的键来排序。如果键是包装类和字符串,那么就会按照升序进行排序。

为什么包装类和字符能排序?是因为他们实现了Comparable接口并且重写了comparaTo方法。

因此我们想通过TreeMap给自定义的类排序,就需要让类实现接口Comparable并且重写comparaTo方法。

TreeMap按键的升序对条目进行排序。
public static void main(String[] args) {
    Map<Integer, String> map = new TreeMap();
    map.put(2, "b");
    map.put(1, "a");
    map.put(5, "l");
    map.put(9, "h");
    map.put(20, "hz");
    System.out.println(map);
    //遍历1
    System.out.println("for-each遍历:");
    for (Map.Entry<Integer, String> entry : map.entrySet()) {
        System.out.print("<" + entry.getKey() + ", ");
        System.out.print(entry.getValue() + ">     ");
    }
    System.out.println("\n通过迭代器遍历:");
    //遍历2
    Set<Map.Entry<Integer, String>> entrySet = map.entrySet();
    Iterator<Map.Entry<Integer, String>> iterator = entrySet.iterator();
    while (iterator.hasNext()) {
        Map.Entry<Integer, String> next = iterator.next();
        System.out.print("<" + next.getKey() + ", ");
        System.out.print(next.getValue() + ">     ");
    }
}

6.7 LinkedHashMap

/**
 * LinkedHashMap,保证插入的顺序
 */
public class TestLinkedHashMap {
    public static void main(String[] args) {
        LinkedHashMap<Integer, String> lhm = new LinkedHashMap<>();
        lhm.put(2, "b");
        lhm.put(1, "a");
        lhm.put(5, "l");
        lhm.put(9, "h");
        lhm.put(20, "hz");
        System.out.println(lhm);
        //遍历
        for (Map.Entry<Integer, String> entry : lhm.entrySet()) {
            System.out.print("<" + entry.getKey() + ", ");
            System.out.print(entry.getValue() + ">     ");
        }
    }
}

HashMap没有维持任何顺序。 TreeMap按键的升序对条目进行排序。 LinkedHashMap保持插入顺序。

7、Set 接口

img

7.1、HashSet类

7.1.1 HashSet基本使用

常用方法与Collection接口中定义的方法一致

特点:

  • 无序 --->原因:因为使用了hashmap作为底层存储,hashmap的hash函数算出来的值不能保证有序
  • 无下标-->使用hash算法算出存储位置,而不是使用下标
  • 不可重复-->因为set的add方法内部调用的是Hashmap的put方法,而我们添加的值是作为 HashMap的key来存储,所以根据hashmap的hash算法,如果同一个key,那么hash函数算出来的值一定相等,只要相等,就不会重复存储,所以就实现了去重。
public static void main(String[] args) {
    //创建一个HashSet对象
    HashSet hashSet = new HashSet();
    hashSet.add(1);
    hashSet.add(1.0);
    hashSet.add("ssss");
    hashSet.add(new String("String"));
    System.out.println(hashSet);
    //删除元素
    hashSet.remove("String");
    System.out.println(hashSet);
    System.out.println(hashSet.contains(1.0));
    //使用迭代器遍历set
    Iterator iterator = hashSet.iterator();
    while (iterator.hasNext()) {
        //hashSet.add(999);//迭代集合时不要做添加或删除操作
        System.out.println(iterator.next());
    }

    //如果想规定同一种Set只能存储同一种数据类型,可以通过泛型来限制
    Set<String> set = new HashSet<>();
    set.add("abc");
    set.add("def");
    set.add("ghk");
    System.out.println(set);
    //使用增强for循环遍历集合
    for (String val : set) {
        System.out.print(val + " ");
    }
    System.out.println();
    //集合存对象
    Set<User1> users = new HashSet<>();
    users.add(new User1("张三", 18));
    users.add(new User1("李四", 19));
    for (User1 user : users)
        System.out.print(user.getName() + "  ");
}

7.1.2 HashSet 去重原理

HashSet底层去重:
/**
 * Set集合的使用,hashset去重原理:因为set的add方法内部调用的是Hashmap的put方法,而我们添加的值是作为
 * HashMap的key来存储,所以根据hashmap的hash算法,如果同一个key,那么hash函数算出来的值一定相等,只要相等,
 * 就不会重复存储,所以就实现了去重。
 */
public class TestHashset {
    public static void main(String[] args) {
        //要使用一个集合,第一步就得先把集合对象创建出来。
        //因为Set是一个接口,具体的实现类是HashSet,所以这儿是一个多态,父类引用指向子类对象
        Set<Order> set = new HashSet();

        //往集合里添加元素,因为我们结合在创建时限制了只能存储Order类型的值,所以需要创建Order对象
        //使用无参构造方法创建一个类
        Order order1 = new Order();
        order1.setName("cloth");
        order1.setId(1);
        set.add(order1);
        //使用带参构造方法创建对象
        set.add(new Order(2, "shoes", 100, 399.0f ));
        set.add(new Order(2, "shoes", 100, 399.0f ));
        //输出集合
        System.out.println(set);
        //使用迭代器遍历集合
        Iterator<Order> iterator = set.iterator();
        while (iterator.hasNext()) {
            Order order = iterator.next();//获取到的是集合里的单个对象
            System.out.print(order.getName() + "      ");
        }
        System.out.println();
        //使用for each遍历
        for (Order o : set) {
            System.out.print(o.getName() + "      ");
        }
        System.out.println();
        //如果需要判断自定义对象是否包含在集合内,我们需要重写equals方法和hashcode方法
        System.out.println(set.contains(new Order(2, "shoes", 100, 399.0f )));

        System.out.println("\n----------------------------------------");
        //set集合里添加基本数据类型
        HashSet<Integer> setInt = new HashSet<>();
        setInt.add(100);
        setInt.add(200);
        setInt.add(300);
        setInt.add(400);
        setInt.add(500);
        System.out.println(setInt);
        //删除元素
        setInt.remove(300);
        setInt.add(100);
        System.out.println(setInt);
        //判断集合是否包含某个元素
        System.out.println(setInt.contains(500));
    }
}

重写hashCodeequals方法

为什么重写equals方法就要重写hashcode方法?

当我们对比两个对象是否相等时,我们就可以先使用 hashCode 进行比较,如果比较的结果是 true,那么就可以使用 equals 再次确认两个对象是否相等,如果比较的结果是 true,那么这两个对象就是相等的,否则其他情况就认为两个对象不相等。这样就大大的提升了对象比较的效率,这也是为什么 Java 设计使用 hashCode 和 equals 协同的方式,来确认两个对象是否相等的原因。

public class Order {
    private Integer id;
    private String name;
    private Integer number;
    private Float price;

    public Order() {
    }

    public Order(Integer id, String name, Integer number, Float price) {
        this.id = id;
        this.name = name;
        this.number = number;
        this.price = price;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getNumber() {
        return number;
    }

    public void setNumber(Integer number) {
        this.number = number;
    }

    public Float getPrice() {
        return price;
    }

    public void setPrice(Float price) {
        this.price = price;
    }

    //重写toString
    @Override
    public String toString() {
        return "{id : " + id + " name:" + name + " number:" + number + "  price:" + price + "}";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Order order = (Order) o;
        return Objects.equals(id, order.id) &&
                Objects.equals(name, order.name) &&
                Objects.equals(number, order.number) &&
                Objects.equals(price, order.price);
    }

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

7.2、LinkedHashSet类

LinkedHashSet

特点:

  • 1、有序 保证插入顺序
  • 2、无下标
  • 3、不可重复

与父类的方法一致,去重的原理,也与父类一致

public class LinkedHashSetDemo {
    public static void main(String[] args) {
        //LinkedHashSet  有序(链表维护顺序)  不能重复
        LinkedHashSet<String> set = new LinkedHashSet<>();
        set.add("jack");
        set.add("大娃");
        set.add("二娃");
        set.add("rose");
        set.add("爷爷");
        set.add("爷爷");
        for (String s : set) {
            System.out.println(s);
        }
        //1、底层实现 (LinkedHashMap)
        //2、去重原理 (与hashSet一致)
    }
}

7.3、TreeSet类

TreeSet特点:

  • 1、TreeSet类似于TreeMap,按升序对元素进行排序。
  • 2、无下标
  • 3、不可重复
public static void main(String[] args) {
    //LinkedHashSet  有序(链表维护顺序)  不能重复
    TreeSet<Integer> set = new TreeSet<>();
    set.add(10);
    set.add(23);
    set.add(5);
    set.add(25);
    set.add(15);
    System.out.println(set);
    TreeSet<User1> users = new TreeSet<>();
    users.add(new User1("张三", 16));
    users.add(new User1("李四", 6));
    users.add(new User1("王五", 26));
    users.add(new User1("赵强", 12));
    users.add(new User1("麻子", 10));
    System.out.println(users);
}

7.3.1 常用方法

HashSet类的方法一致

特点:

  • 使用TreeSet集合存储对象的时候,对象必须要实现Comparable接口

7.3.2 实现原理

TreeSet在存储元素的时候,会调用compareTo方法。两个作用:

  • 1、排序: 返回值大于0升序,返回值小于0降序
  • 2、去重(返回值为0) TreeSet认为返回0,两个对象就是相同的对象
package com.qf.demo03;

import java.util.TreeSet;

public class TreeSetDemo {
    public static void main(String[] args) {
        //TreeSet的去重原理
        TreeSet<Person> set = new TreeSet<Person>();
        set.add(new Person("admin","123"));
        set.add(new Person("aa","bb"));
        set.add(new Person("jack","123"));
        set.add(new Person("cxk","abc"));
        set.add(new Person("rose123","123"));
        set.add(new Person("admin","123")); 

        //如果两个对象的用户名和密码都相等,则认为就是相同的对象,且按照名字长度升序存放
        for (Person person : set) {
            System.out.println(person);
        }
        /**
         *   TreeSet集合在保存对象元素的时候,对象必须要实现Comparable接口重写compareTo方法。
         *   TreeSet的去重原理为:如果compareTo方法的返回值为0,则认为是相同的对象
         *   如果compareTo方法的返回大于0则是升序排序,小于0则是降序排序
         */

    }
}
class Person implements Comparable<Person>{
    String username;
    String password;
    public Person(String username, String password) {
        super();
        this.username = username;
        this.password = password;
    }
    public Person() {
    }
    @Override
    public String toString() {
        return "User [username=" + username + ", password=" + password + "]";
    }

    @Override
    public int compareTo(Person o) {
        if(!this.username.equals(o.username)) {
            return this.username.length() - o.username.length();
        }else {
            if(this.password.equals(o.password)) {
                return 0;
            }else {
                return this.username.length() - o.username.length();
            }
        }
    }
}

栈(Stack)

  1. 栈(stack)是一种操作受限的线性序列

    1. 只能在栈的顶部插入和删除数据
    2. 底部为盲端
  2. 抽象数据类型(stack)
接口名描述
push(int val)把元素入栈,val表示要入栈的值
pop()出栈元素
top()查看栈顶元素
size()获取栈大小
empty()判断栈是否为空
  1. 栈是一种后进先出(LIFO)或先进后出(FILO)的数据结构
public static void main(String[] args) {
    Stack stack = new Stack();//底层基于数组实现
    //入栈元素
    stack.push("星期二");
    stack.push(12);
    System.out.println("stack : " + stack);
    //出栈元素
    //Object obj = stack.pop();
    //查看栈顶元素
    Object obj1 = stack.peek();
    //System.out.println("obj : " + obj);
    System.out.println("obj1 : " + obj1);
}

队列(Queeu)

  1. 队列(queue)也是一种操作受限的线性序列

    1. 只能在队尾插入数据
    2. 只能在队头删除数据
  2. 抽象数据类型(Queue)
接口名描述
enqueue(int val)把元素入队,val表示要入队的值
dequeue()出队元素
size()获取队列大小
empty()判断队列是否为空
  1. 队列是一种先进先出(FIFO1)或后进后出(LIFO)的数据结构
public static void main(String[] args) {
    Queue<String> queue = new PriorityQueue();//多态
    //队列插入元素的操作叫做"入队"
    queue.add("星期二");
    queue.add("星期天");
    System.out.println(queue);
    //取元素的过程叫做"出队"
    System.out.println(queue.poll());
}

Collections工具类

集合: 工具类(Collections)

  • Collections.reverse(List<?> list)
  • Collections.shuffle(List<?> list)
  • Collections.sort(List<?> list)
public class CollectionsDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("jack");
        list.add("大娃");
        list.add("二娃");
        list.add("rose");
        list.add("妖怪");
        list.add("蛇妖");
        list.add("蛇妖");
        System.out.println(list);
        //按照字典顺序
        Collections.sort(list);
        System.out.println(list);

        //将集合元素进行翻转
        Collections.reverse(list);
        System.out.println(list);

        //将集合中的元素进行随机打乱
        Collections.shuffle(list);
        System.out.println(list);

        //Arrays数组工具类      Collections集合工具类
    }
}

Comparable

/**
 * Comparable是一个接口,我们要让工具类给我们的自定义类按照某种属性排序,就要实现该接口里的comparaTo方法
 */
public class TestComparable {
    public static void main(String[] args) {
        //创建一个集合
        ArrayList<JavaStudent> arrayList = new ArrayList<>();
        arrayList.add(new JavaStudent("wangyi"));
        arrayList.add(new JavaStudent("zhangsan"));
        arrayList.add(new JavaStudent("lisi"));
        //使用集合的排序工具类lections给我们的集合排序
        Collections.sort(arrayList);
        System.out.println(arrayList);
    }
}

class JavaStudent implements Comparable<JavaStudent> {
    private String username;

    public JavaStudent(String username) {
        this.username = username;
    }
    
    /**
     * 对于表达式x.compareTo(y),如果返回值为0,则表示x和y相等,如果返回值大于0,则表示x大于y,如果返回值小于0,则表示x小于y.
     * 在ComparableTimSort类里的binarySort(Object[] a, int lo, int hi, int start)方法里有以下if逻辑:
     * if (pivot.compareTo(a[mid]) < 0)//这部分逻辑就会来调用自定义的compareTo方法
     */
    @Override
    public int compareTo(JavaStudent o) {
        System.out.println(this.username.compareTo(o.username));
        return -this.username.compareTo(o.username);
    }
    
    /*
    首先按照名字排序,如果名字相等,就按照年龄排序。
    @Override
    public int compareTo(Person001 o) {
        int result = 0;
        result = this.name.compareTo(o.name);
        if (result == 0) {
            result = this.age - o.age;
        }
        return result;
    }   
    */

    @Override
    public String toString() {
        return "JavaStudent{" +
                "username='" + username + '\'' +
                '}';
    }
}

Comparator

是一个接口,内部提供了compare抽象方法,让用户自定义的类可以通过实现该接口重写compare方法来实现按照指定字段排序。开发中通常会会为每个字段写一个比较器,如下代码示例,通过实现Comparator匿名内部类的形式,我们为username和age字段实现了不同的比较器。

/**
 * Comparable是一个接口,我们要让工具类给我们的自定义类按照某种属性排序,就要实现该接口里的comparaTo方法
 */
public class TestComparable {
    public static void main(String[] args) {
        //创建一个集合
        ArrayList<JavaStudent> arrayList = new ArrayList<>();
        arrayList.add(new JavaStudent("wangyi", 13));
        arrayList.add(new JavaStudent("zhangsan", 18));
        arrayList.add(new JavaStudent("lisi", 15));
        //使用集合的排序工具类lections给我们的集合排序
        //Collections.sort(arrayList);
        //System.out.println(arrayList);
       
        //使用Comparator来按照指定属性排序
        //使用匿名内部类重写比较器
        /*Collections.sort(arrayList, new Comparator<JavaStudent>() {
            @Override
            public int compare(JavaStudent o1, JavaStudent o2) {
                return o1.getUsername().compareTo(o2.getUsername());
            }
        });*/
        //使用显示类重写比较器,等价于上边的匿名内部类形式
       /* Collections.sort(arrayList, new UsernameComparator());
        System.out.println(arrayList);*/

        //按照年龄排序,因为年龄是int类型,所以没有自带的comparaTo方法
       Collections.sort(arrayList, new Comparator<JavaStudent>() {
            @Override
            public int compare(JavaStudent o1, JavaStudent o2) {
                return o1.getAge() - o2.getAge();
            }
        });
        System.out.println(arrayList);
    }
}

/**
 * 实现Comparator接口重写compara方法实现比较器
 */
class UsernameComparator implements Comparator<JavaStudent> {

    @Override
    public int compare(JavaStudent o1, JavaStudent o2) {
        return o1.getUsername().compareTo(o2.getUsername());
    }
}

/**
 * 实现Comparable接口重写comparaTo方法实现比较器
 */
class JavaStudent implements Comparable<JavaStudent> {
    private String username;
    private int age;



    public JavaStudent(String username, int age) {
        this.username = username;
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * 对于表达式x.compareTo(y),如果返回值为0,则表示x和y相等,如果返回值大于0,则表示x大于y,如果返回值小于0,则表示x小于y.
     * 在ComparableTimSort类里的binarySort(Object[] a, int lo, int hi, int start)方法里有以下if逻辑:
     * if (pivot.compareTo(a[mid]) < 0)//这部分逻辑就会来调用自定义的compareTo方法
     */
    @Override
    public int compareTo(JavaStudent o) {
        return this.username.compareTo(o.username);
    }

    /*
    首先按照名字排序,如果名字相等,就按照年龄排序。
    @Override
    public int compareTo(Person001 o) {
        int result = 0;
        result = this.name.compareTo(o.name);
        if (result == 0) {
            result = this.age - o.age;
        }
        return result;
    }
    */

    @Override
    public String toString() {
        return "JavaStudent{" +
                "username='" + username + '\'' +
                ", age=" + age +
                '}';
    }
}

泛型使用

泛型类

泛型类 类名
public class Box<T> {  //T:表示任意的java类型       E、K、V
    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

public class MyGeneric<E> {//此处E代表任意类型
    private Object[] names = new Object[10];
    //定义下标记录数组和有效元素个数
    int index = 0;
    //使用数组作为底层存储
    /**
     * 提供一个add方法 往数组内插入元数
     */
    public void add(E val) {
        names[index++] = val;
    }
}
public class TestMyGeneric{
    public static void main(String[] args) {
        MyGeneric generic  = new MyGeneric();
        //插入数据
        generic.add("你好");
        //如果我们想让MyGeneric可以支持插入任意数据类型。让我们MyGeneric支持泛型即可
        generic.add("12");
    }
}

泛型接口

泛型接口 接口名
public interface MyInterface<T> {
    
}
public interface GenericInterface<T> {
    void m1(T t);
}

public class GenericInterfaceImpl implements GenericInterface<String>{
    @Override
    public void m1(String s) {

    }
}
class GenericInterfaceImpl1 implements GenericInterface {

    @Override
    public void m1(Object o) {

    }
}

public class GenericInterfaceTest {
    public static void main(String[] args) {
        GenericInterface gi = new GenericInterfaceImpl();
        gi.m1("12");
        GenericInterface<Integer> gif = new GenericInterfaceImpl1();
        gif.m1(12);
    }
}

泛型方法

泛型方法 public T 方法名(T t,...){}
//泛型可以作为参数,(必须得先定义  <T> )
public <T> void m1(T t) {

}

/**
 * 自定义泛型方法
 */
public class TestMyGeneric{
    public static void main(String[] args) {
        m1(102);
        m1("102");
        m1(true);
        System.out.println(m2("nel"));
    }


    //定义无返回值泛型方法
    public static <E> void m1(E e) {
        System.out.println(e);
    }
    //定义返回值泛型方法
    public static <E> E m2(E e) {
        return (E)(new String(e.toString()));
    }
}

泛型上下边界

泛型上下边界

  • 语法:
public class Demo01 {
    //? 表示不确定类型     此时的?表示Object
    public static void test01(List<?> list) {
    }
    public static void test02(List<? extends Number> list) {
    }
    public static void test03(List<? super Number> list) {
    }

    public static <T> void test04(List<? extends Comparable<T>> list) {
    }
    public static void main(String[] args) {
        List<String> list1 = new ArrayList<String>();
        List<Integer> list2 = new ArrayList<Integer>();
        List<Number> list3 = new ArrayList<Number>();
        List<Object> list4 = new ArrayList<Object>();
        test01(list1);
        test01(list2);
        test01(list3);
        test01(list4);

        //test02(list1);  //错误,方法定义泛型的上边界,泛型只能是Number及其Number子类
        test02(list2);
        test02(list3);
        //test02(list4);  //错误,方法定义泛型的上边界,泛型只能是Number及其Number子类 

        //test03(list1);  //错误,方法定义泛型的下边界,泛型只能是Number及其Number父类
        //test03(list2);
        test03(list3);
        test03(list4);------------------

        test04(list1);
        test04(list2);
        //test04(list3);    //错误,方法定义泛型的上边界,泛型必须实现Comparable接口
        //test04(list4);      //错误,方法定义泛型的上边界,泛型必须实现Comparable接口
    }    
}

泛型擦除

当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如<T><?>的类型参数都被替换为Object。

img

当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number><? extends Number>的类型参数被替换为Number<? super Number>被替换为Object。

img

擦除方法定义中的类型参数原则和擦除类定义中的类型参数一样

img

十一、异常

之前我们介绍的基本类型、类、接口、枚举数据,操作的过程中可能有很多出错的情况,出错的原因可能是多方面的,有的是不可控的内部原因,比如内存不够了、磁盘满了,比如网络连接有问题,更多的可能是程序的编写错误,比如引用变量未初始化就直接调用实例方法。这些非正常情况在Java中统一被认为是异常,Java使用异常机制来统一处理。

什么是异常

异常指不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等。程序如果不处理这些异常,可能会造成意外的结果或是程序终止

Java中的异常

异常在Java中是一个对象。下图是异常的类继承关系图。

img

Error和Exception

Error是Throwable的一个子类,表示应用程序不应尝试捕获的严重问题。如:内存溢出,内存泄露等。

Exception是Throwable的子类,代表应用程序在编译期间或执行过程中应该捕获的问题。

Checked(编译期) 和 Unchecked(运行时)的异常

Checked:受检查异常,也叫非运行时异常或编译期异常,是指程序在编译期间就会检查。该类异常必须在写代码时就要求程序员捕获处理,否则程序不能正常编译。

  • 例如: CloneNotSupportedException, IOException, SQLException, InterruptedException, FileNotFoundException

Unchecked: 运行时异常,只有在程序真正执行的时候才会检测的异常,程序能正常编译生产字节码文件。

  • 例如: IllegalMonitorStateException, ConcurrentModificationException, NullPointerException, IndexOutOfBoundException, NumberFormatException

异常关键字

  • try – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
  • catch – 用于捕获异常。catch用来捕获try语句块中发生的异常。
  • finally – finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
  • throw – 用于抛出异常。
  • throws – 用在方法签名中,用于声明该方法可能抛出的异常。

如何处理异常

异常通常使用术语叫做“捕获异常”。处理的方法通常有:

  • try-catch
  • try-catch-finally
  • try-finally
  • try-with-resource

try-catch

块用于处理 Java 中的异常。语法try...catch

try {
  // code
}catch(Exception e) {
  // code
}

//构造一个空指针异常
String s = null;
//通过try...catch捕获空指针异常,通常空指针异常是不推荐使用异常捕获的,而是使用程序逻辑判断处理掉
try {
    //正常业务逻辑代码
    System.out.println(s.length());
    System.out.println("调用字符串长度方法结束");
} catch (NullPointerException e) {
    //想查看具体是什么类型的异常
    e.printStackTrace();//打印出错的堆栈信息。
    //出现异常后改怎么处理
    //System.out.println("此处调用获取字符串长度的方法抛出了空指针异常");
}

在这里,我们将可能产生异常的代码放在try块中。每个try块后面都有一个catch块。当异常发生时,它被catch块捕获。没有try块就不能使用catch块,反之亦如此。

示例:

public class TestTryCatch {
  public static void main(String[] args) {
    try {
    
      int divideByZero = 5 / 0;
      System.out.println("try块中的其余代码");
      
    }catch (ArithmeticException e) {
      System.out.println("ArithmeticException => " + e.getMessage());
    }
  }
}

在示例中,我们试图将一个数字除以0。在这里,此代码生成异常。

为了处理异常,我们将代码5 / 0放在try块内。现在,当发生异常时,块内的其余代码将被跳过。该catch块捕获异常并执行 catch 块内的语句。如果try块中没有任何语句产生异常,则跳过该catch块。

try-catch-finally

try {
    //程序代码。可能出现异常
} catch(Exception e) {
    //捕获异常
} finally {
    //一定会执行的代码
}

//除零异常
int a = 10;
try {
    int b = 0;
    System.out.println("b : " + b);
} catch (ArithmeticException e) {
    System.out.println("异常信息");
} finally {
    System.out.println("finally");
}
System.out.println("-------------------");
try {
    int b = 10 / 0;//构造除零异常
    System.out.println("b : " + b);
} catch (ArithmeticException e) {
    System.out.println("异常信息");
} finally {
    System.out.println("finally");
}
InputStream is = null;
try {
    is = new FileInputStream("");
    //code
    //is.close();//如果程序再关闭文件流过程中发生异常,那么文件流就不能被正常关闭。
    System.out.println(is);
} catch (Exception e) {
    e.printStackTrace();
    System.out.println("catch");
} finally {
    try {
        is.close();
    } catch (IOException e) {
        System.out.println("关闭文件出现异常");
        e.printStackTrace();
    }
}

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

执行的顺序

  • 当try没有捕获到异常时:try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句;
  • 当try捕获到异常,catch语句块里没有处理此异常的情况:当try语句块里的某条语句出现异常时,而没有处理此异常的catch语句块时,此异常将会抛给JVM处理,finally语句块里的语句还是会被执行,但finally语句块后的语句不会被执行;
  • 当try捕获到异常,catch语句块里有处理此异常的情况:在try语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句;

try-finally

在Java中,无论是否有异常,finally块始终会被执行到。

finally块是可选的。对于每个try块,有且只能有一个finally块。

finally块的基本语法是:

try {
  //code
} finally {
  // finally block always executes
}

示例:

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

public static int m() {
    try {
        System.out.println("程序开始运行");
        //System.exit(0);//虚拟机停止工作。
        return 1;
        //System.out.println("end");
    } finally {
        System.out.println("finally");
    }
    //System.out.println("main end");
}

注意:使用finally块是一个好习惯。这是因为它可以包含重要的清理代码,例如。

  • 可能被 return、continue 或 break 意外跳过的代码
  • 关闭文件或连接

多个 catch

对于每个try块,可以有多个catch块。多个catch块允许我们以不同的方式处理每个异常。

每个catch块的参数类型表示它可以处理的异常类型。例如,

public static void main(String[] args) {
    InputStream is = null;
    try {
        is = new FileInputStream("");//编译期异常
        int a = 10 / 0;//运行时异常
    } catch (FileNotFoundException e) {
        System.out.println("文件未找到");
    } catch (ArithmeticException e) {
        System.out.println("除零异常");
    } finally {
        try {
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    System.out.println("其他任务");
}

class ListOfNumbers {
  public int[] arr = new int[10];
    public void writeList() {
        try {
          arr[10] = 11;
        }catch (NumberFormatException e1) {
          System.out.println("NumberFormatException => " + e1.getMessage());
        }catch (IndexOutOfBoundsException e2) {
          System.out.println("IndexOutOfBoundsException => " + e2.getMessage());
        }
    }
}

public class TestMultiCatch {
  public static void main(String[] args) {
    ListOfNumbers list = new ListOfNumbers();
    list.writeList();
  }
}

输出

IndexOutOfBoundsException => 索引 10 超出长度 10 的范围

在这个例子中,我们创建了一个arr大小为10的整数数组。

由于数组索引从0开始,因此数组的最后一个元素位于arr[9]。注意声明,

arr[10] = 11;

在这里,我们尝试为索引10分配一个值。因此,IndexOutOfBoundException发生。

try块中发生异常时,

  • 异常被抛出到第一个catch块。第一个catch块不处理IndexOutOfBoundsException,所以它被传递到下一个catch块。
  • 上面示例中的第二个catch块是适当的异常处理程序,因为它处理IndexOutOfBoundsException

捕获多个异常

catch从 Java SE 7 及更高版本开始,我们现在可以用一个块捕获不止一种类型的异常。

这减少了代码重复并提高了代码的简单性和效率。

可以由catch块处理的每种异常类型都使用竖线分隔|,该种方式可以代替多个catch块

语法是:

try {
  // code
} catch (ExceptionType1 | Exceptiontype2 ex) { 
  // catch block
}

示例:

public class Main {
  public static void main(String[] args) {
    try {
      int array[] = new int[10];
      array[10] = 30 / 0;
    } catch (ArithmeticException | ArrayIndexOutOfBoundsException e) {
      System.out.println(e.getMessage());
    }
  }
}

捕获基类异常Exception

class Main {
  public static void main(String[] args) {
    try {
      int array[] = new int[10];
      array[10] = 30 / 0;
    } catch (Exception e) {
      System.out.println(e.getMessage());
    }
  }
}

捕获基异常类和子异常类

class Main {
  public static void main(String[] args) {
    try {
      int array[] = new int[10];
      array[10] = 30 / 0;
    } catch (Exception | ArithmeticException | ArrayIndexOutOfBoundsException e) {
      System.out.println(e.getMessage());
    }
  }
}

在这个例子中,ArithmeticExceptionArrayIndexOutOfBoundsException都是类Exception的子类。所以,会得到一个编译错误。

final、finally、 finalize的区别

final可以用来修饰类、方法、变量,分别有不同的意义,final修饰的class代表不可以继承扩展,final的变量是不可以修改的,而final的方法也是不可以重写的(override)。

finally则是Java保证重点代码一定要被执行的一种机制。我们可以使用try-finally或者try-catch-finally来进行类似关闭JDBC连接、保证unlock锁等动作。 finalize是基础类java.lang.Object的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize机制现在已经不推荐使用,并且在JDK 9开始被标记为deprecated。

抛出异常throw和throws

  1. throw关键字用于显式抛出单个异常对象。

当我们throw出现异常时,程序的流程从try块移动到catch块。

示例:

public class TestThrow {
    public static void divideByZero() {
        throw new ArithmeticException("Trying to divide by 0");
    }
    
    public static void main(String[] args) throws ArithmeticException {
    
        //第一种,捕获接到的异常
       /* try {
            divideByZero();//该方法会抛出一个异常对象,我们有两种处理方式
        } catch (ArithmeticException e) {
            System.out.println(e.getMessage());// / by zero
        }*/
        //第二种,把接到的异常继续抛到上一层,使用关键字throws
        divideByZero();
    }
}

在上面的例子中,我们明确地抛出了ArithmeticException使用throw关键字。

  1. throws关键字用于在方法上抛出一个异常

示例:Java throws 关键字

import java.io.*;

class Main {
    public static void findFile() throws IOException {
        File newFile = new File("test.txt");
        FileInputStream stream = new FileInputStream(newFile);
      }
    
      public static void main(String[] args) {
        try {
          findFile();
        } catch (IOException e) {
          System.out.println(e);
        }
      }
}

当我们运行这个程序时,如果文件test.txt不存在,则FileInputStream抛出一个继承自IOException类的FileNotFoundException

findFile()方法指定抛出一个IOExceptionmain()方法调用此方法并在抛出异常时处理该异常。

如果方法不处理异常。好的编程习惯是不要在main方法里在把异常向上抛。

try-with-resouces 语法糖

该语法是jdk1.7开始实现的。此语法用来代替在finally语句块内关闭资源的语法形式,该语法糖只能使用到实现了AutoCloseable接口的具体类上。try后边圆括号内可以创建多个对象。

语法结构:

try(定义打开文件对象,当程序正常或者异常结束时,都会自动关闭打开的资源) {
    //正常业务逻辑代码
} catch(Exception e) {
    //发生异常后的处理逻辑
}

public class Test01 {
    public static void main(String[] args) {
        InputStream is = null;
        try {
            is = new FileInputStream("");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            //关闭流
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        try(FileInputStream ism = new FileInputStream("");
            FileOutputStream fous = new FileOutputStream("")
            ){
            //读文件内容
            //关闭文件流
            System.out.println(ism);
        }catch (IOException e) {
            e.printStackTrace();
        }

        //自定义实现AutoCloseable接口的类。
        try(AutoClose ac = new AutoClose();) {
            System.out.println(ac);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //以上代码反编译后的代码如下
        try{
            AutoClose ac = new AutoClose();
            System.out.println(ac);
            ac.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

class AutoClose implements AutoCloseable{
    public void close() throws IOException {}
}

自定义异常

  • 1、自定义运行时异常
  • 2、自定义编译期异常
  • 应用场景:比如用户系统登录,出现用户名或者密码错误,可以使用自定义异常进行区分。今后学习MVC模型的时候,底层代码出现问题需要告知上层代码,也有很多使用自定义异常的场景。
/**
 * 自定义异常
 */
public class AgeException extends RuntimeException {
    public AgeException() {
        super();//调用父类无参构造方法
    }
    public AgeException(String msg) {
        super(msg);//调用父类接收string类型参数的构造方法
    }
}
使用自定义异常
public static void main(String[] args) {
    try{
        m1(130);
    } catch (AgeException e) {
        System.out.println(e.getMessage());
    }

}

public static void m1(int age) {
    if (age > 125) {
        //超过正常年龄范围,抛出异常。
        throw new AgeException("年龄不合法");
    } else {
        System.out.println("正常年龄");
    }
}

注意:子类重写父类的方法,不能抛出比父类更大的异常。

异常实践

  1. 尽量不要捕获 RuntimeException

阿里巴巴 Java 开发手册上这样规定:

尽量不要 catch RuntimeException,比如 NullPointerException、IndexOutOfBoundsException 等等,应该用预检查的方式来规避。

public static void main(String[] args) {
    //判断数组是否为空和数组内容是否为空
    if (args != null && args.length != 0) System.out.println("");
    String s = null;//""
    m(s);
}

public static void m(String s) {
    //判断字符串引用是否为空,字符串内容是否为空
    if ( s != null && !s.isEmpty()) return;
    //System.out.println(s.length());
}
  1. 尽量使用 try-with-resource 来关闭资源
try(FileInputStream ism = new FileInputStream("");
            FileOutputStream fous = new FileOutputStream("")
            ){
            //读文件内容
            //关闭文件流
            System.out.println(ism);
        }catch (IOException e) {
            e.printStackTrace();
        }
  1. 不要捕获 Throwable
  2. 不要省略异常信息的记录
  3. 不要记录了异常又抛出了异常
try{
    int a = 9 / 0;
} catch (Throwable t) {
    System.out.println("除零错误");//记录异常
    throw new ArithmeticException();//抛出异常
}
  1. 不要在 finally 块中使用 return

如果在finally代码块中使用了return,try代码块中的执行结果就不会被正常返回。以下m方法希望返回10,但由于finally中又return,所以结果会返回100。

public static int m(String s) {
    try{
        return 10;
    } finally {
        return 100;
    }
}

阿里巴巴 Java 开发手册上这样规定:

try 块中的 return 语句执行成功后,并不会马上返回,而是继续执行 finally 块中的语句,如果 finally 块中也存在 return 语句,那么 try 块中的 return 就将被覆盖。

  1. 捕获具体的子类而不是捕获 Exception 类
  2. finally 块中永远不要抛出任何异常
  3. 不要使用 printStackTrace() 语句或类似的方法

应该尽可能的自定义异常的信息。

  1. 只抛出和方法相关的异常

作业

1、Java 中所有的错误都继承自______类;在该类的子类中,______类表示严重的底层错误,对于这类错误一般处理的方式是______;______类表示例外、异常。

2、异常类 java.rmi.AlreadyBoundException,从分类上说,该类属于______(已检查|运行时)异常,从处理方式上说,对这种异常______处理。 异常类 java.util.regex.PatternSyntaxException,从分类上说,该类属于______(已检查|运行时)异常,从处理方式上说,对这种异常______处理。

十二、线程与并发

线程共享代码、全局变量、堆区;独享栈和寄存器。

1 概念

1.1 进程和线程

  • 什么是进程?

一个执行中的程序

img

  • 什么是线程?

img

  • 区别

线程是进程中的一个实体,线程本身是不会独立存在的。

进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。

操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是CPU分配的基本单位。

在Java中,当我们启动main函数时其实就启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程。

1.2 为什么需要多线程

众所周知,CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:

  • CPU 增加了缓存,以均衡与内存的速度差异;(由此产生了可见性问题)
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;(由此产生原子性问题)
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。(由此产生有序性问题)

1.3 多线程的利弊

利:

  • 资源利用率更好
  • 程序设计在某些情况下更简单
  • 程序响应更快

弊:

  • 使程序设计更复杂
  • 增加线程切换开销
  • 资源开销增大

2 Java中创建线程【重点】

2.1 创建线程的三种方式

  • 继承Thread类并重写run的方法创建,
  • 实现Runnable接口的run方法创建,
  • 实现Callable和Future接口创建线程(这种创建方式在学完线程池后才能讲解)

2.2 通过继承Thread类

/**
 * 通过继承创建一个线程
 */
public class InheritCreateThread {
    public static void main(String[] args) {
        //创建自定义线程对象
        MyThread01 thread01 = new MyThread01();
        //调用start方法,启动线程
        thread01.start();//启动线程必须要通过start()方法,而不能直接调用run方法
        //thread01.run();//这叫做方法调用,而不是启动线程。
        System.out.println(Thread.currentThread().getName() + " Main thread end");
    }
}

/**
 * 通过继承Thread创建一个自定义线程,重写Thread类的run方法来实现线程的创建
 */
class MyThread01 extends Thread {
    @Override
    public void run() {
        //打印线程id或线程名
        System.out.println(Thread.currentThread().getName() + "子线程启动");
    }
}

2.3 实现Runnable接口

/**
 * 测试类
 */
public class ImplementsRunnableCreateThread {
    public static void main(String[] args) {
        //启动实现Runnable接口的自定义线程
        //第一:创建自定义线程对象
        Mythread02 runnable = new Mythread02();
        //第二:创建一个Thread对象,并且把自定义线程对象当做Thread的构造参数传入
        Thread thread = new Thread(runnable);
        //第三:调用Thread的start方法启动线程
        thread.start();

        Thread thread1 = Thread.currentThread();
        System.out.println(thread1.getName() + " ==> " + thread1.getId());
    }
}
/**
 * 通过实现接口Runnable,并重写内部的run方法实现自定义线程
 */
class Mythread02 implements  Runnable {
    @Override
    public void run() {
        //打印当前线程的名字和id号
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + " ==> " + thread.getId());
    }
}

多种启动线程的写法

/**
 * 测试类
 */
public class ImplementsRunnableCreateThread {
    public static void main(String[] args) {
        //启动实现Runnable接口的自定义线程
        //第一:创建自定义线程对象
        Mythread02 runnable = new Mythread02();
        //第二:创建一个Thread对象,并且把自定义线程对象当做Thread的构造参数传入
        Thread thread = new Thread(runnable);
        //第三:调用Thread的start方法启动线程
        thread.start();

        Thread thread1 = Thread.currentThread();
        System.out.println(thread1.getName() + " ==> " + thread1.getId());
        //启动线程二
        Thread thread2 = new Thread(new Mythread02());
        thread2.start();
        //启动线程三
        new Thread(new Mythread02()).start();
        //通过匿名内部类启动线程四
        new Thread(new Runnable() {
            @Override
            public void run() {
                //打印当前线程的名字和id号
                Thread thread = Thread.currentThread();
                System.out.println(thread.getName() + " ==> " + thread.getId());
            }
        }).start();
    }
}
/**
 * 通过实现接口Runnable,并重写内部的run方法实现自定义线程
 */
class Mythread02 implements  Runnable {
    @Override
    public void run() {
        //打印当前线程的名字和id号
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + " ==> " + thread.getId());
    }
}

2.4 两者的区别

  • 使用继承Thread类的方式创建多线程
优势:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势:线程类已经继承了Thread类,所以不能再继承其他父类。
  • 采用实现Runnable接口的方式创建多线程
优势:线程类只是实现了Runnable接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势:编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

启动线程需要注意的问题

  • 1、不要调用run方法
  • 2、一个线程只能调用一次start方法,如果调用多次会出异常java.lang.IllegalThreadStateException

3 线程基础

  • 1、设置线程名称 (setName、getName、Thread.currentThread获取当前线程对象)
  • 2、设置线程的优先级 (setPriority、getPriority),不一定生效
  • 3、线程休眠 (Thread.sleep(毫秒数))
  • 4、线程礼让(Thread.yield()),但不一定成功
  • 5、线程加入(join()) 如果线程出现join操作,那么调用的线程将会阻塞。等到被调用的线程执行结束。

sleep

Thread.sleep(),该方法在哪个线程内部使用,就会让哪个线程睡眠。

public class TestThreadSleep {
    public static void main(String[] args) throws InterruptedException {
        //匿名内部类启动线程
        new Thread(new Runnable()  {
            @Override
            public void run() {
                //让线程睡眠3秒钟,注意:此处的时间是按照毫秒计算的。所以1秒等于1000毫秒
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //打印当前线程的名字和id号
                Thread thread = Thread.currentThread();
                System.out.println(thread.getName() + " ==> " + thread.getId());
            }
        }).start();
        Thread.sleep(5000);//让主线程睡眠5秒
        Thread thread1 = Thread.currentThread();
        System.out.println(thread1.getName() + " ==> " + thread1.getId());
    }
}

yield

public class TestYield implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) { 
            if (i % 5 == 0) {
                System.out.println(Thread.currentThread() + " yield cpu.....");
                Thread.yield();
            }
        }
        System.out.println(Thread.currentThread() + " is over");
    }

    public static void main(String[] args) {
        new Thread(new TestYield()).start();
        new Thread(new TestYield()).start();
        new Thread(new TestYield()).start();
    }
}

join

public class TestJoin {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }
        });
        thread.setName("sub thread one");
        thread.start();

        Thread.currentThread().setName("main thread");
        try {
            thread.join();//阻塞等待被调线程任务执行结束才会继续往下执行。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //在主线程中运行的代码
        for (int i = 100; i < 110; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

线程的状态

初始状态 ------ 就绪状态 ------- 运行状态 ----- 限期等待 -----无限期等待 -------- 终止状态

img

线程中断

定义:一个线程在执行完任务后会自动结束,如果在运行过程中发生异常也会提前结束。

interrupt()

通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。

public class TestInterrupt {
    private static class MyThread1 extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
                System.out.println(Thread.currentThread().getName() + " run");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new MyThread1();
        thread.start();
        thread.interrupt();
        System.out.println("Main Thread run");
    }
}

interrupted()

如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。

但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。

public class TestInterrupted {
    private static class MyThread2 extends Thread {
        @Override
        public void run() {
            System.out.println(interrupted());
            while (!interrupted()) {
                // ..
            }
            System.out.println(Thread.currentThread().getName() + " end");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread2 = new MyThread2();
        thread2.start();
        thread2.interrupt();
        System.out.println("Main Thread end");
    }
}

线程分类

daemon线程(守护线程)比如垃圾回收线程,jvm不会等待此类线程结束自己才退出。

user线程(用户线程)只要有一个用户线程还没结束,正常情况下JVM就不会退出。

注意:通常自定义的线程都属于用户线程。

守护线程

创建守护线程

public static void main(String[] args) {        
    Thread daemonThread = new Thread(new  Runnable() {            
        public void run() {
        }        
    });        
    //设置为守护线程        
    daemonThread.setDaemon(true);        
    daemonThread.start();    
}

用户线程

public static void main(String[] args) {        
    Thread thread = new Thread(new  Runnable() {            
        public void run() {                
            for(; ; ){}            
        }       
    }); 
     //设置为守护线程        
     //thread.setDaemon(true);        
    //启动子线程        
    thread.start();
    System.out.print("main thread is over");  
}

4 并发编程【重点】

在程序设计的角度,希望通过某些机制让计算机可以在一个时间段内,执行多个任务。

线程安全问题

共享资源

所谓共享资源,就是同一份资源被多个线程所持有或者说多个线程访问。线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题

因此在访问共享资源时需要要保证同步,保证被操作资源(比如变量)的原子性。

private static int shared = 0;
private static void incrShared(){
//自增操作会分为三步完成,
//第一:把shared的数据读取到cpu寄存器中
//第二:把shared的值加1
//第三:把自增后的值写回到内存
    shared++;
}
static class ChildThread extends Thread {
    @Override
    public void run() {
        incrShared();
    }
}
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new ChildThread();
    Thread t2 = new ChildThread();
    t1.start();
    t2.start();
    System.out.println(shared);
}

使用同步处理以上代码输出正确结果

public class TestShare {
    private static int shared = 0;

    /**
     * 给自增操作加锁,保证自增操作的原子性。
     */
    private synchronized static void incrShared(){
        shared++;
    }
    static class ChildThread extends Thread {
        @Override
        public void run() {
            incrShared();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new ChildThread();
        Thread t2 = new ChildThread();
        t1.start();
        t2.start();
        //在打印前保证子线程都执行完了自增操作
        t1.join();
        t2.join();
        System.out.println(shared);
    }
}

线程同步synchronized

java中使用关键字synchronized来实现线程的同步,该关键字对应两条jvm指令,monitor enter 和 monitor exit。

0: aconst_null
         1: astore_1
         2: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         5: aload_1
         6: invokevirtual #3                 // Method java/lang/String.length:()I
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        12: return
同步方式
  • 方式一:同步代码块
  • 方式二:同步方法

语法形式

同步成员方法
public void m1() {
    synchronized(this) {
        //代码块
    }
}
//以上同步方式等价于如下
public synchronized void m2() {
}

//同步静态代码块
public static void m1() {
    synchronized(this) {
        //代码块
    }
}
//以上同步方式等价于如下
public synchronized static void m2() {
}
线程同步买票案例

线程不安全

  • 循环中的代码并非原子操作,所以在线程执行的过程中,会有其他线程执行,会造成ticket < 0无效
  • ticket-- 并非原子操作,其包含有三个步骤
class TicketRunnable implements Runnable{
     int ticket = 100;
    @Override
    public void run() {
        while(true) {
            if(ticket < 0) {
                break;
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //获取线程的名称
            System.out.println(Thread.currentThread().getName()+"卖出了:"+ ticket-- +"号票");
        }
    }
}

同步代码块解决

public class TestTicket {
    public static void main(String[] args) {
        new Thread(new TicketRunnable()).start();
        new Thread(new TicketRunnable()).start();
        //new Thread(new TicketRunnable()).start();
    }
}

class TicketRunnable implements Runnable{
    static int ticket = 100;//100张票
    @Override
    public void run() {
        while(true) {
            //所有的线程都必须要获取到同一把锁之后才能运行卖票和减票两步操作。这就保证了两步操作的原子性。
            //线程一旦获取到锁之后,会一直知道执行完synchronized代码块里的逻辑才会释放该锁。
            synchronized(TicketRunnable.class) {
                if (ticket < 1) {//如果无票就跳出循环,线程执行结束
                    break;
                }
                //获取线程的名称
                System.out.println(Thread.currentThread().getName() + "卖出了:" + ticket-- + "号票");
            }
        }
    }
}

同步方法解决

class TicketRunnable implements Runnable{
    static int ticket = 50;
    final Object obj = new Object();
    @Override
    public void run() {
        while(true) {
            sale();
        }
    }

    //同步方法的锁对象是this,所以要保证当前对象的唯一
    public synchronized(this) void sale() {
        if(ticket < 0) {
            System.exit(0);
        }
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //获取线程的名称
        System.out.println(Thread.currentThread().getName()+"卖出了:"+ ticket-- +"号票");
    }
}
public static void main(String[] args) {
    TicketRunnable ticketRunnable = new TicketRunnable();
    for (int i = 0; i < 2; i++)
        new Thread(ticketRunnable).start();
}

synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的wait系列方法时释放该内置锁。内置锁是排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。另外,由于Java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而synchronized的使用就会导致上下文切换

线程活跃问题

线程死锁[理解]

什么是死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

img

死锁示例
public class TestDeadLock {
    public static void main(String[] args) {
        Boy boy = new Boy();
        Girl girl = new Girl();
        boy.start();
        girl.start();
    }
}

class  MyLock{
    static Object left = new Object(); //左筷子
    static Object right = new Object(); //右筷子
}

class Boy extends Thread{
    @Override
    public void run() {
        synchronized (MyLock.left) { //拥有左筷子   锁
            System.out.println("boy获取到了左筷子,等待右筷子");
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (MyLock.right) {
                System.out.println("boy可以吃饭");
            }
        }
    }
}
class Girl extends Thread{
    @Override
    public void run() {
        synchronized (MyLock.right) {
            System.out.println("Girl拥有右筷子,等待左筷子");
            synchronized (MyLock.left) {
                System.out.println("Girl可以吃饭");
            }
        }
    }
}
如何避免死锁?

各线程之间访问资源保持顺序性, 如上的男孩女孩获取筷子的示例,我们只需要让男孩线程和女孩线程获取筷子的顺序保持一致,就可以避免死锁。

public class TestDeadLock {
    public static void main(String[] args) {
        Boy boy = new Boy();
        Girl girl = new Girl();
        boy.start();
        girl.start();
    }
}

class  MyLock{
    static Object left = new Object(); //左筷子
    static Object right = new Object(); //右筷子
}

class Boy extends Thread{
    @Override
    public void run() {
        synchronized (MyLock.left) { //拥有左筷子   锁
            System.out.println("boy获取到了左筷子,等待右筷子");
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (MyLock.right) {
                System.out.println("boy可以吃饭");
            }
        }
    }
}
class Girl extends Thread{
    @Override
    public void run() {
        synchronized (MyLock.left) {
            System.out.println("Girl拥有左筷子,等待右筷子");
            synchronized (MyLock.right) {
                System.out.println("Girl可以吃饭");
            }
        }
    }
}

效率问题

创建线程:因为创建实现需要操作系统从用户态切换到内核态去申请内存空间,然后又要切换回用户态。

线程上下文切换:如果线程过多,那么cpu大部分时间都用在切换线程上下文上,而不是用来执行任务,造成资源浪费。

img

synchronized锁升级

synchronized的使用通常有三种形式

关于临界区。所谓“临界区”,指的是某一块代码区域,它同一时刻只能由一个线程执行。在上面的例子中,如果synchronized关键字在方法上,那临界区就是整个方法内部。而如果是使用synchronized代码块,那临界区就指的是代码块内部的区域。

锁的状态总共有四种

  • 无锁 无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。也就是CAS(CAS是基于无锁机制实现的)ComparaAndSwap
  • 偏向锁 偏向锁是在单线程执行代码块时使用的机制,如果在多线程并发的环境下,则一定会转化为轻量级锁或者重量级锁。
  • 轻量级锁 当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁优点减少传统的重量级锁使用操作系统互斥量产生的性能消耗靠多次CAS实现的,效率高
  • 重量级锁 Synchronized是通过对象内部的一个叫做 监视器锁(Monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到内核态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。Mutex(又叫 Lock),在多线程中,作为同步的基本类型,用来保证没有两个线程或进程同时在他们的关键区域

自旋锁

因为挂起线程以及恢复线程要转移到操作系统内核模式执行,这会给性能带来极大的影响。

在JDK定义中,自旋锁默认的自旋次数为10次,用户可以使用参数-XX:PreBlockSpin来更改。

自适应锁

在JDK 1.6中引入了。获取锁的自旋次数不确定,根据之前的数据来确认自旋次数或者不自旋,让JVM变得更聪明。

锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行。变量是否逃逸,对于虚拟机来说是需要使用复杂的过程间分析才能确定的,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下还要求同步呢?这个问题的答案是:有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中出现的频繁程度也许超过了大部分读者的想象。我们来看看如下例子,这段非常简单的代码仅仅是输出三个字符串相加的结果,无论是源代码字面上,还是程序语义上都没有进行同步。

public String concatString(String s1, String s2, String s3) {
    return s1 + s2 + s3;
}

String是一个不可变的类,对字符串的连接操作总是通过生成新的String对象来进行的,因此Javac编译器会对String连接做自动优化。在JDK 5之前,字符串加法会转化为StringBuffer对象的连续append()操作,在JDK 5及以后的版本中,会转化为StringBuilder对象的连续append()操作,以上代码会变成如下代码

public String concatString(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer()
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}

现在大家还认为这段代码没有涉及同步吗?每个StringBuffer.append()方法中都有一个同步块,锁 就是sb对象 。 虚拟机观察变量sb , 经过逃逸分析后会发现它的动态作用域被限制在concatString( ) 方 法内部 。 也就是sb的 所有引用都永远不会逃逸到 concatString( )方法之外 , 其他线程无法访问到它 , 所以这里虽然有锁,但是可以被安全地消除掉。在解释执行时这里仍然会加锁,但在经过服务端编译器的即时编译之后,这段代码就会忽略所有的同步措施而直接执行。

锁粗化

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据 的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变少,即使存在锁竞争,等 待锁的线程也能尽可能快地拿到锁。 大多数情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如上代码连续的append()方法就属于这类情况。如果虚拟机探测到有这样一串零碎的操作 都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,扩展到第一个append()操作之前直至最后一个append()操作之后,这样只需要加锁一次就可 以了。

使用Synchronized有哪些要注意的?

  • 锁对象不能为空,因为锁的信息都保存在对象头里
  • 作用域不宜过大,影响程序执行的效率。
  • 避免死锁(多个线程获取锁的顺保持一致)
  • 在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键字,因为代码量少,避免出错

synchronized是公平锁吗?

synchronized实际上是非公平的,新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待,这样有利于提高性能,但是也可能会导致饥饿现象

5 Lock锁

在jdk1.5之前实现线程同步只能使用synchronized和volatile两个关键字。因为synchronized是关键字,所以在使用上受限,使用synchronized实现同步,那么在等待过程中的其他线程会一直等待下去,没有机制打断他们,这很容易产生死锁。另外,synchronized是jvm支持,相对来说锁比较重。volatile关键字只能保证可见性和有序顺(禁止指令重排序从而保证顺序性),但是它不能保证原子型。保证原子性需要使用synchronized。

  • ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁

的线程会被阻塞而被放入一个阻塞队列里面。

  • JDK1.5加入,与synchronized比较,显示定义,结构更灵活。
  • 提供更多实用性方法,功能更强大、性能更优越。

Lock锁

ReentrantLock:

  • Lock接口的实现类,与synchronized一样具有互斥锁功能。

语法格式

Lock lock = new ReentrantLock;
lock.lock;//加锁
try {
    //业务逻辑
} finally {
    lock.unlock();//释放锁
}
class TicketRunnable implements Runnable {
    static int ticket = 10000;//100张票
    //这种方法创建锁不能保证多个线程拿到的是同一把锁,所以不能保证数据不会被打乱
    //要保证多个线程获取的是同一把锁,就需要使用静态初始化方式
    //Lock lock = new ReentrantLock();
    static Lock lock = new ReentrantLock();//静态初始化一把锁。

    @Override
    public void run() {
        //加锁
            while (true) {
                System.out.println("lock" + lock);
                lock.lock();
                try {            
                    //所有的线程都必须要获取到同一把锁之后才能运行卖票和减票两步操作。这就保证了两步操作的原子性。
                    //线程一旦获取到锁之后,会一直知道执行完synchronized代码块里的逻辑才会释放该锁。
                    if (ticket < 1) {//如果无票就跳出循环,线程执行结束
                        break;
                    }
                    //获取线程的名称
                    System.out.println(Thread.currentThread().getName() + "卖出了:" + ticket-- + "号票");
                } finally {
                    lock.unlock();//程序无论正常还是异常结束,都要包装锁被释放
                }
            }
        System.out.println("release lock");
    }
}

随堂练习:用 ReentrantLock来实现一个简单的线程安全的 list

读写锁

ReentrantReadWriteLock:

  • 一种支持一写多读的同步锁,读写分离,可分别分配读锁、写锁。
  • 支持多次分配读锁,使多个读操作可以并发执行。

互斥规则:

  • 写-写:互斥,阻塞。
  • 读-写:互斥,读阻塞写、写阻塞读。
  • 读-读:不互斥、不阻塞。
  • 在读操作远远高于写操作的环境中,可在保障线程安全的情况下,提高运行效率。
public class TestReadWriteLock {
    static User user = new User();
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        //8个线程读
        for (int i = 0; i < 8; i++) {
            new Thread(new ReadThread()).start();
        }
        //2个线程写
        for (int i = 0; i < 2; i++) {
            new Thread(new WriteThread()).start();
        }
    }

}

class ReadThread implements Runnable{
    @Override
    public void run() {
        System.out.println(TestReadWriteLock.user.getName());
    }
}
class WriteThread implements Runnable{
    @Override
    public void run() {
        TestReadWriteLock.user.setName("cxk");
    }
}

class User{
    String name;
    //创建读写锁对象
    static ReadWriteLock rwl = new ReentrantReadWriteLock();
    //读锁
    static Lock readLock = rwl.readLock();
    //写锁
    static Lock writeLock = rwl.writeLock();

    public void setName(String name) {
        try {
            writeLock.lock();
            System.out.println(Thread.currentThread().getName() + " 正在写数据");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.name = name;
        } finally {
            writeLock.unlock();
        }
    }
    public String getName() {
        try {
            readLock.lock();
            System.out.println(Thread.currentThread().getName() + " 正在读数据");
            return name;
        }finally {
            readLock.unlock();
        }
    }
}

重入锁

重入锁:一个线程获取到一个对象的锁之后还可以继续再次获得该锁,对象头有一个专门记录获得锁次数的标记。

  • 重入锁也叫作递归锁,指的是同一个线程外层方法获取到一把锁后,内层方法同样具有这把锁的控制权限
  • synchronized和Lock锁都可以实现锁的重入
public class TestReentrantLock {    
    public static void main(String[] args) {
        //启动线程
        new Thread(new MyRunnable()).start();
    }
}
class MyRunnable implements Runnable{
    @Override
    public void run() {
        a();
    }
    //当前锁对象为this
    public synchronized void a() {
        System.out.println("a");
        b();
    }
    //当前锁对象为this
    public synchronized void b() {
        System.out.println("b");
        c();
    }
    public synchronized void c() {
        System.out.println("c");
    }
}

公平锁

根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁 的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。 而非公平锁则在运行时闯入,也就是先来不一定先得 。

ReentrantLock 提供了公平和非公平锁的实现 。

·公平锁: ReentrantLockpairLock =new ReentrantLock(true)。
·非公平锁: ReentrantLockpairLock=new ReentrantLock(false)。 

如果构造函数不传递参数,则默认是非公平锁 。

例如,假设线程 A 已经持有了锁,这时候线程 B 请求该锁其将会被挂起。 当线程 A 释放锁后,假如 当前有线程 C 也需要获取该锁,如果采用非公平锁方式,则根据线程调度 策略, 线程 B 和线程 C 两者之一可能获取锁,这时候不需要任何其他干涉,而如果使用 公平锁则需要把C挂起,让B获取当前锁 。

在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销 。

synchronized的缺陷

效率低:锁的释放情况少,只有代码执行完毕或者异常结束会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时

不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活

无法知`道是否成功获得锁`,相对而言,Lock可以拿到状态,如果成功获取锁,....,如果获取失败,

Lock解决相应问题

Lock类有以下4个方法:

  • lock(): 加锁
  • unlock(): 解锁
  • tryLock(): 尝试获取锁,返回一个boolean值
  • tryLock(long,TimeUtil): 尝试获取锁,可以设置超时
public class ReentrantLockList {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(3);
        MyList myList = new MyList();
        service.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int i = 0; i < 10; i++) {
                    myList.add(i);
                }
            }
        });
        service.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int i = 10; i < 20; i++) {
                    myList.add(i);
                }
            }
        });
        service.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int i = 20; i < 30; i++) {
                    myList.add(i);
                }
            }
        });
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(myList.list);
        service.shutdown();
    }
}

class MyList {
    List<Integer> list = new ArrayList<>();
    //创建锁
    Lock lock = new ReentrantLock();

    public void add(int val) {
        if (lock.tryLock()) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(val);
            lock.unlock();
        } else {
            System.out.println(Thread.currentThread().getName() + "获取锁失败");
        }

    }
}

synchronized锁只与一个条件(是否获取锁)相关联,多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。ReentrantLock的lockInterruptibly()方法可以优先考虑响应中断。 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待。有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了

Synchronized和ReentrantLock

除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

7 线程通信[了解]

线程通信:线程之间可以通过共享内存的方式通信。

若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产的产品放入缓冲区中,消费者从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个满的缓冲区中放入产品。

  • wait() 调用该对象的某个线程会阻塞挂起
  • notify() 调用该对象的notify方法会唤醒在该对象上调用wait等待中的线程。具体是哪个线程,是随机的。
  • notifyAll()
/**
 * 生产者消费者
 */
public class ProducerAndConsumer {
    static AppStore store = new AppStore();
    public static void main(String[] args) {
        //创建一个商店对象
        ;
        //生产者
        for (int p = 0; p < 5; p++)
        new Thread(new Runnable() {
            @Override
            public void run() {
                //生产商品
                for (int i = 0; i < 20; i++) {
                    store.production(i);
                }
            }
        }).start();

        //消费者
        for (int q = 0; q < 5; q++)
        new Thread(new Runnable() {
            @Override
            public void run() {
                //消费商品
                for (int j = 0; j < 20; j++) {
                    store.consumption();
                    //打印
                }
            }
        }).start();
    }
}

//存放商品的区域
class AppStore<T> {
    //定义一个放接收到的商品的仓库,仓库必须要保证多个对象之间只能有一个。
    static final Queue queue = new ConcurrentLinkedQueue();

    //接收生产者生产的商品
    public synchronized void production(T goods) {
        //当仓库满了时不允许在生产,规定仓库只能存放10个商品
        while (queue.size() > 10) {
            System.out.println("仓库已满,需要等待");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //把商品放入仓库
        queue.add(goods);
        System.out.println("把商品 " + goods + " 存到库存中");
        //仓库有了商品后需要通知消费者开始消费
        this.notifyAll();
    }

    //把商品提供给消费者
    public synchronized void consumption() {
        //当仓库没货的时候需要等待
        while (this.queue.size() == 0) {
            try {
                System.out.println("仓库没货需要等待");
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("消费了商品 : " + queue.poll());
        //仓库有了多余空间就通知生产者线程开始生产
        this.notifyAll();
    }
}

Sleep和wait的区别:sleep会抱着锁睡,wait进入等待时会释放自己持有的锁。

8 线程的生命周期[重点]

线程的5种状态:创建状态---->就绪状态---->运行状态---->阻塞\等待状态---->终止状态

img

9 线程池

概念

  • 如果有非常的多的任务需要多线程来完成,且每个线程执行时间不会太长,这样频繁的创建和销毁线程。
  • 频繁创建和销毁线程会比较耗性能。有了线程池就不要创建更多的线程来完成任务,因为线程可以重用
  • 线程池用维护者一个队列,队列中保存着处于等待(空闲)状态的线程。不用每次都创建新的线程。

线程池实现逻辑

img

假设我们线程池里只有5个线程可用,但这时候来了50个请求,我们该怎么处理?

img

3.3 线程池中常见的类

常用的线程池接口和类(所在包java.util.concurrent)。

Executor:线程池的顶级接口。

ExecutorService:线程池接口,可通过submit(Runnable task) 提交任务代码。

Executors工厂类:通过此类可以获得一个线程池。

方法名描述newFixedThreadPool(int nThreads)获取固定数量的线程池。参数:指定线程池中线程的数量。newCachedThreadPool()获得动态数量的线程池,如不够则创建新的。

/**
 * 使用线程池技术来执行自定义的线程任务
 */
public class TestThreadPool01 {
    //第二步:定义一个测试方法
    public static void main(String[] args) {
        //第三步:使用线程池工厂获取一个线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);
        //第四步:创建自定义线程的一个对象
        MyTask01 task01 = new MyTask01();
        //第五步:把自定义线程对象的引用通过线程池的submit方法提交给线程池执行
        executor.submit(task01);
        //打印主线程名
        System.out.println(Thread.currentThread().getName());
        //第六步:关闭线程池
        executor.shutdown();
    }
}

/**
 * 第一步:自定义一个线程类,基于实现runnable接口的形式
 */
class MyTask01 implements Runnable {
    @Override
    public void run() {
        System.out.println("这是通过线程池启动的一个任务" + Thread.currentThread().getName());
    }
}

public class TestExcutor {
    public static void main(String[] args) {
        //创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(3);
        //往线程池内提交任务(线程)
        service.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });
        //通过继承Thread创建的线程提交给线程池。
        service.submit(new TT());
        System.out.println(Thread.currentThread().getName());
        //注意:一定要关闭线程池
        service.shutdown();
    }
}

class TT extends Thread {
    @Override
    public void run() {
        System.out.println("extends thread");
    }
}
/**
 * 使用无边界线程池执行线程任务
 */
public class TestNewCachedThreadPool01 {
    public static void main(String[] args) {
        //通过线程池工厂类或一个线程池
        ExecutorService service = Executors.newCachedThreadPool();
        //往线程池里添加任务,任务通过匿名内部类的形式提供
        service.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        });
        service.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        });
        //使用完线程池一定要记得关闭
        service.shutdown();
    }
}

线程池内部最重要一个构造函数各参数的含义

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
corePoolSize : 核心线程数
maximumPoolSize:最大线程数
keepAliveTime:线程空闲时间
TimeUnit: 时间单位
workQueue:任务队列,线程忙不过来时任务就放到该队列内
threadFactory:线程工厂
handler:拒绝策略:有四种

Callable接口

  • JDK1.5加入,与Runnable接口类似,实现之后代表一个线程任务。
  • Callable具有泛型返回值、可以声明异常。

public interface Callable< V >{ public V call() throws Exception; }

public class TestCallable1 {
    public static void main(String[] args) {
        //1、创建线程池对象
        ExecutorService es = Executors.newFixedThreadPool(2);
        //2、通过线程池提交线程并执行任务
        es.submit(new MyCallable());
        //3、关闭线程池
        es.shutdown();
    }
}
class MyCallable implements Callable{
    @Override
    public Object call() throws Exception {

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

        }
        return null;
    } 
}

Future接口

  • Future接口表示将要执行完任务的结果。
  • get()以阻塞形式等待Future中的异步处理结果(call()的返回值)。
public class TestCallable2 {
    /**
     * Runnable接口和Callable接口的区别?
     *         1、这两个接口都可以当做线程任务提交并执行
     *      2、Callable接口执行完线程任务之后有返回值,而Runnable接口没有返回值
     *      3、Callable接口中的call方法已经抛出了异常,而Runnable接口不能抛出编译异常
     *  Future接口:    
     *      用于接口Callable线程任务的返回值。
     *      get()方法当线程任务执行完成之后才能获取返回值,这个方法是一个阻塞式的方法   
     *      
     *   随堂案例:
     *       使用两个线程,并发计算1-100的和,   一个线程计算1~50,另一个线程计算51~100, 最终汇总结果    
     */

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //1、创建线程池对象
        ExecutorService es = Executors.newFixedThreadPool(2);
        //2、通过线程池提交线程并执行任务
        Future<String> future = es.submit(new MyCallable1());
        //获取线程任务的返回值
        System.out.println(future.get());
        System.out.println("哈哈哈哈哈哈");
        //3、关闭线程池
        es.shutdown();
    }
}


class MyCallable1 implements Callable<String>{
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }

        return "这是Callable线程任务的返回值";
    }

}
案例:计算1-1000结果,使用四个线程分别计算?即:第一个线程计算1-250 第二个 251~500 ...
public class Test01 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //1、创建线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        //2、提交两个线程任务
        Future<Integer> f1 = es.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 0; i < 51; i++) {
                    sum = sum + i;
                }
                return sum;
            }
        });
        Future<Integer> f2 = es.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 51; i < 101; i++) {
                    sum += i;
                }
                return sum;
            }
        });

        System.out.println(f1.get() + f2.get());
        //3、关闭线程池
        es.shutdown();
    }
}

10 线程安全的集合

集合结构

通过Collections获取线程安全集合

Collections工具类中提供了多个可以获得线程安全集合的方法。

方法名public static Collection synchronizedCollection(Collection c)public static List synchronizedList(List list)public static Set synchronizedSet(Set s)public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)public static SortedSet synchronizedSortedSet(SortedSet s)public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)

CopyOnWriteArrayList

  • 线程安全的ArrayList,加强版读写分离。
  • 写有锁,读无锁,读写之间不阻塞,优于读写锁。写写互斥
  • 写入时,先copy一个容器副本、再添加新元素,最后替换引用。
  • 使用方式与ArrayList无异。
CopyOnWriteArrayList<String> list=new CopyOnWriteArrayList<>();

CopyOnWriteArraySet

  • 线程安全的Set,底层使用CopyOnWriteArrayList实现。
  • 唯一不同在于,使用addIfAbsent()添加元素,会遍历数组。
  • 如存在元素,则不添加(扔掉副本)。
CopyOnWriteArraySet<String> set=new CopyOnWriteArraySet<>();

ConcurrentHashMap

JDK1.7实现

  • 初始容量默认为16段(Segment),使用分段锁设计。
  • 不对整个Map加锁,而是为每个Segment加锁。
  • 当多个对象存入同一个Segment时,才需要互斥。
  • 最理想状态为16个对象分别存入16个Segment,并行数量16。
  • 使用方式与HashMap无异。

img

JDK1.8实现

  • 内部使用CAS交换算法+synchronized实现多线程并发安全

img

11 CAS

CAS 方式为乐观锁,synchronized 为悲观锁。因此使用 CAS 解决并发问题通常情况下性能更优。

JDK 里面的 Unsafe提供了一个方法

boolean compareAndSwapLong(Object obj,long i,long expect, long update)

其中compareAndSwap的意思是比较并交换。CAS有四个操作数, 分别为:对象引用 、 对象中的变量 、 变量预期值,更新后的值 。 其操作含义是 , 如果 对象 obj 中 valueOffset的变量值为 expect,则使用新的值 update替换 旧的值 expect。 这是处理器提供的一个原子性指令。

CAS只能保证一个共享变量的原子操作

我们以一个简单的例子来解释这个过程:

  1. 如果有一个多个线程共享的变量i原本等于5,我现在在线程A中,想把它设置为新的值6;
  2. 我们使用CAS来做这个事情;
  3. 首先我们用i去与5对比,发现它等于5,说明没有被其它线程改过,那我就把它设置为新的值6,此次CAS成功,i的值被设置成了6;
  4. 如果不等于5,说明i被其它线程改过了(比如现在i的值为2),那么我就什么也不做,此次CAS失败,i的值仍然为2。

那有没有可能我在判断了i为5之后,正准备更新它的新值的时候,被其它线程更改了i的值呢?

不会的。因为CAS是一种原子操作,它是一种系统原语,是一条CPU的原子指令,从CPU层面保证它的原子性

12 ABA问题

关于CAS操作有个经典的ABA问题,因为CAS需要在操作值的时候,检查值有没有发生变化,比如没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时则会发现它的值没有发生变化,但是实际上却变化了。

ABA 问题的产生是因为变量 的状态值产生 了环形转换,就是变量的值可 以从 A 到 B,然后再从 B 到 A。如果变量的值只能朝着一个方向转换 ,比如 A 到 B, B 到 C, 不构成环形,就不会存在 问题。 JDK 中AtomicStampedReference 类给每个变量的状态值都配备了一个时间戳, 从而避免了ABA问题的产生。

十三、I/O流

文件(File)

File类

介绍

概念:表示操作系统磁盘上的文件或者是文件夹

路径:

  • 相对路径 (相对于当前工程的根路径)
  • 绝对路径 (在磁盘上的完整路径)

常见方法

方法名描述createNewFile()创建一个新文件。mkdir()创建一个新目录。如果父目录不存在,则无法创建mkdirs()创建一个新目录。如果父目录不存在,则一起创建delete()删除文件或空目录。exists()判断文件或目录是否存在length()获取文件(夹)的大小(字节)getAbsolutePath()获取文件的绝对路径。getAbsoluteFile()获取文件(夹)的绝对路径:(返回File)getName()获取当前file对象的文件名或者是文件夹名getParent()获取当前file对象的父目录(返回String)isDirectory()是否是目录。isFile()是否是文件。getPath()获取文件(夹)的相对路径:(返回String)listFiles()列出目录中的所有内容。

//创建File对象
File file = new File("d:\\a.txt");
if(!file.exists()) {
    //创建文件
    file.createNewFile();
}
System.out.println("判断文件或者是文件夹是否存在"+file.exists());
System.out.println("判断是否是文件:"+file.isFile());
System.out.println("判断是否是文件夹:"+file.isDirectory());
System.out.println("获取文件或者文件夹的名字:"+file.getName());
System.out.println("获取文件大小(字节):"+file.length());
System.out.println("获取文件的相对路径:"+file.getPath());
System.out.println("获取文件的绝对路径(String):"+file.getAbsolutePath());
System.out.println("获取文件的绝对路径(File):"+file.getAbsoluteFile());
System.out.println("获取文件的父目录(String):"+file.getParent());
System.out.println("获取文件的父目录(File):"+file.getParentFile());


System.out.println("获取文件所在位置磁盘的总空间:"+file.getTotalSpace());
System.out.println("获取文件所在位置磁盘的可用空间:"+file.getFreeSpace());
System.out.println("获取文件的最后修改时间(毫秒)"+file.lastModified());
System.out.println("判断文件是否可读"+file.canRead());
System.out.println("判断文件是否可写"+file.canWrite());
System.out.println("判断文件是否可执行"+file.canExecute());
System.out.println("判断文件是否是隐藏文件"+file.isHidden());

File file = new File("./");//斜线(/)表示根目录, ./表示当前目录
System.out.println(file.getAbsolutePath());

public static void main(String[] args) throws IOException {
    //创建一个File文件对象
    File file1 = new File("/Users/fcp/aaaa.txt");//构造参数填写文件的绝对路径
    //File file = new File("D:\\Users\\fcp\\test.txt"); windows
    //使用文件对象的引用调用创建文件方法来创建文件
    //System.out.println(file1.createNewFile());

    //创建File对象
    File file2 = new File("/Users/fcp/jay111/jay");
    //File file2 = new File("c:\\Users\\fcp\\jay"); windows
    //创建目录
    //System.out.println(file2.mkdir());
    //System.out.println(file2.mkdirs());

    File file3 = new File("/Users/fcp/aaaa.txt");
    //System.out.println(file3.delete());

    File file4 = new File("/Users/fcp/test");
    //删除有文件的目录
    //System.out.println(file4.delete());
    File file5 = new File("/Users/fcp/");
    //判断目录是否存在
    System.out.println(file5.exists());

    File file6 = new File("/Users/fcp/test.txt");
    //判断文件大小
    System.out.println(file6.length());

    File file7 = new File("io/test01.java");
    System.out.println(file7.getAbsolutePath());
    System.out.println(file7.getAbsoluteFile());
    System.out.println(file6.getName());
    System.out.println(file6.getParent());
    //获取文件相对路径
    System.out.println("相对路径:" + file7.getPath());
    //d:\a\b\c\e.txt
    //d:\a\b\c\test.java
    //new File("e.txt");

    m(file6);
}

public static void m(File file) {
    //此处不知道file是文件还是目录
    //首先判断file是否存在
    if (file.exists()) {
        //如果存在,再次判断是文件还是目录
        if (file.isDirectory())
            System.out.println("是一个目录");
        else
            System.out.println("是一个文件");
    }
}

FilenameFilter接口

FilenameFilter:文件过滤器接口

  • boolean accept(File pathname)。
  • 当调用File类中的listFiles()方法时,支持传入FilenameFilter接口实现类,对获取文件进行过滤,只有满足条件的文件的才可出现在listFiles()的返回值中。

案例:使用文件过滤器获取所有java文件

File[] files = f.listFiles(new FilenameFilter() {
    @Override
    public boolean accept(File dir, String name) {
        return name.endsWith(".java");
    }
});
for (File file2 : files) {
    System.out.println(file2.getName());
}

文件搜索案例

遍历出某文件夹中的所有的java文件
public class FileDemo02 {
    public static void main(String[] args) {
        getJavaFile(new File("d:\\zz"));
    }
    //思考:删除一个文件夹    

    //思考:获取文件夹下所有的java文件
    public static void getJavaFile(File file) {
        //遍历当前文件夹下所有的文件或者是文件
        File[] files = file.listFiles();
        //f有两种情况,文件或者是文件夹
        for (File f : files) {
            if(f.isFile()) {
                if(f.getName().endsWith(".java")) {
                    System.out.println(f.getName());
                }
            }else {
                //递归调用getJavaFile
                getJavaFile(f);
            }
        }
    }    
}

I/O流概念

IO 即 Input/Output,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如磁盘,网络等)的过程称为输出。数据传输过程类似于水流,因此称为 IO 流。从传输方向上:IO 流可分为输入流和输出流,从数据处理方式上:可分为字节流和字符流。

java中所有的流都继承自四个基类:

  • InputStream/Reader: 字节输入流 / 字符输入流。
  • OutputStream/Writer: 字节输出流 / 字符输出流。

四个基类有几十个子类。下图列出部分子类。

字节流

img

InputStream

字节输入流,因为InputStream是一个抽象类,不能具体实例化,所以需要创建该类的一个子类对象,才能实现文件的读取。常用子类:FileInputStream此类按字节从指定路径文件读取数据,每次读一个字节,所以读取速度相对较慢,如果想让读取速度加快,我们可以使用缓冲流:BufferedInputStream

/**
 * 使用文件字节输入流读取数据
 */
public class TestFileInputStream {
    public static void main(String[] args) {

        //创建文件字节流对象
        File file = new File("outputstream.txt");
        //InputStream is = new FileInputStream(file);
        try(InputStream is = new FileInputStream(file);) {
            //使用对象引用调用读取数据的方法
            //第一种读取文件数据的方式
            int b = 0;
            while ((b = is.read()) != -1) {//如果读取的数据不等于-1表示文件内容未读完
                //System.out.print(b);//读取到的整数对应字符编码表里的数字
                System.out.print((char) b);//通过强制类型转换把整数转成字符
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println();
        try(InputStream is = new FileInputStream(file);) {
            //第二种读取文件的方式,创建一个数组暂存读取到的字节数据
            byte[] bytes = new byte[32];
            //int count = is.read(bytes);//count表示读取了多少个字节
            while (is.read(bytes) != -1) {
                System.out.println(new String(bytes));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

public class TestBufferedInputStream {
    public static void main(String[] args) {
        File file = new File("/Users/fcp/Downloads/常用类2.mp4");
        //创建字节缓冲流对象
        //try(InputStream is = new FileInputStream(file)) {
        try(InputStream is = new BufferedInputStream(new FileInputStream(file))) {
            //开始时间
            long start = System.currentTimeMillis();
            //使用字节流读一个视频文件
            int b = 0;
            //定义一个1MB的字节数组
            //byte[] bytes = new byte[1024];
            int count = 0;
            while ((b = is.read()) != -1) {
                count++;//记录循环读取了多少次
            }
            System.out.println("count : " + count);
            //读取文件所花时间
            System.out.println(System.currentTimeMillis()-start);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

OutputStream

字节输出流,常用两个类分别是:FileOutputStream和BufferedOutputStream。后者是前者的缓冲流。

/**
 * 字节输出流
 */
public class TestOutput {

    public static void main(String[] args) {
        //文件字节输出流
        //fileOutput();
        //inputOutput();
        bufferedOutput();
    }
    /**
     * 文件字节输出流:输出具体内容到指定文件,按照一个字节一个字节的方式输出。
     */
    public static void fileOutput() {
        //创建文件字节输出流对象
        //当字符串指定的文件不存在时会自动帮我们创建出来该文件,但如果是目录就不会自动创建
        try(FileOutputStream fos = new FileOutputStream("fileoutput.txt", false);) {
            fos.write(97);//存储到文件中的是该整数对应的字符
            fos.write("你说把爱渐渐放下会走更远\n".getBytes());//按照字节数组输出到指定文件
            fos.write("你说把爱渐渐放下会走更远\n".getBytes(), 3, 6);//要输出字节数组中的哪部分内容,通过数组下标指定
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 通过InputStream读取内容,然后通过OutputStream写到指定文件内
     */
    public static void inputOutput() {

        try(FileInputStream fis = new FileInputStream("/Users/fcp/Downloads/雨下一整晚.mp3");
            FileOutputStream fos = new FileOutputStream("雨下一整晚.mp3")) {
            //读文件内容
            /*int b = 0;//一个字节一个字节的读
            while ((b = fis.read()) != -1) {
                //写入到指定文件
                fos.write(b);
            }*/

            //上边按照字节读太慢,我们只用数组作为缓冲区来读写文件
            byte[] bytes = new byte[1024 * 10];
            while (fis.read(bytes) != -1) {
                fos.write(bytes);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 字节输出缓冲流
     */
    public static void bufferedOutput() {
        try(BufferedInputStream bis = new BufferedInputStream(new FileInputStream("/Users/fcp/Downloads/常用类2.mp4"));
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("常用类2.mp4"));) {
            int b = 0;
            while ( (b = bis.read()) != -1) {
                bos.write(b);
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

字符流

Reader

主要用于读取文本文件。常用类FileReader, BufferedReader

/**
 * 字符输入流
 */
public class TestReader {
    public static void main(String[] args) {
        //创建字符输入流对象
        try(Reader reader = new FileReader("javase/src/io/TestSerializable.java");) {
            //循环读取文件内容
            /*int b = 0;
            while ((b = reader.read()) != -1) {
                System.out.print((char) b);
            }*/
            //通过数组方式读
            char[] buf = new char[512];
            while (reader.read(buf) != -1)
                System.out.println(new String(buf));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Writer

主要用于写字符到指定文件内,常用类FileWriter, BufferedWriter

public class TestWriter {
    public static void main(String[] args) {
        //创建字符输出流对象
        try(Writer writer = new FileWriter("filewriter.txt")) {
            writer.write("明明就");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

BufferedReader和BufferedWriter代码示例

public class TestBuffereReaderAndBufferedWriter {
    public static void main(String[] args) throws IOException {
        //使用BufferedReader和BufferedWriter拷贝文本文件
        BufferedReader br = new BufferedReader(new FileReader("/Users/fcp/Downloads/JavaSE.md"));
        BufferedWriter bw = new BufferedWriter(new FileWriter("JavaSE.txt"));
        //读文件
        /*String val = null;
        while ((val = br.readLine()) != null) {
            bw.write(val);
        }*/

        /*int b = 0;
        while ((b = br.read()) != -1)
            bw.write(b);*/

        char[] chars = new char[1024];
        while (br.read(chars) != -1)
            bw.write(chars);
            bw.flush();
    }

}

以下代码示例演示了使用字符流拷贝mp3文件,最终文件被损坏,无法播放

/**
 * 使用字符流读取媒体文件看是否会损坏文件
 */
public class TestReaderWriter {
    public static void main(String[] args) {
        copyMp3();
    }

    public static void copyMp3() {
        try(FileReader fr = new FileReader("/Users/fcp/Downloads/雨下一整晚.mp3");
            FileWriter fw = new FileWriter("雨下一整晚.mp3")) {
            int b = 0;
            while ((b = fr.read()) != -1) {
                fw.write(b);
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

字节流和字符流的区别

  • 字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件。用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本文件。
  • 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。

操作对象划分

文件流

FileInputStream

int b;
try(FileInputStream fis1 = new FileInputStream("/Users/fcp/test.txt");) {
    while ((b = fis1.read()) != -1) {
        System.out.print((char) b);
    }
} catch (IOException e) {
    e.printStackTrace();
}

FileOutputStream

try(FileOutputStream fos = new FileOutputStream("/Users/fcp/test.txt");) {
    fos.write("海鸥不再眷恋大海 可以飞更远。".getBytes());
} catch (IOException e) {
    e.printStackTrace();
}

FileReader

int b = 0;
try(FileReader fileReader = new FileReader("/Users/fcp/test.txt");) {
    while ((b = fileReader.read())!=-1) {
        // 自动提升类型提升为 int 类型,所以用 char 强转
        System.out.print((char)b);
    }
} catch (IOException e) {
    e.printStackTrace();
}

FileWriter

try(FileWriter fileWriter = new FileWriter("/Users/fcp/test.txt");) {
    fileWriter.write("远方传来风笛 我只在意有你的消息。".toCharArray());
} catch (IOException e) {
    e.printStackTrace();
}

缓冲流

CPU 很快,它比内存快 100 倍,比磁盘快百万倍。程序和内存交互会很快,和硬盘交互相对较慢,这就会导致性能问题。

为了减少程序和硬盘的交互,提升程序的效率,就引入了缓冲流,也就是类名前缀带有 Buffer 的那些,如: BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter。

数组流(了解)

try(InputStream is =new BufferedInputStream(new ByteArrayInputStream(
                "但故事的最后你好像还是说了拜拜".getBytes(StandardCharsets.UTF_8)));) {
    byte[] flush =new byte[1024];
    int len =0;
    while(-1!=(len=is.read(flush))){
        System.out.println(new String(flush,0,len));
    }
} catch (IOException e) {
    e.printStackTrace();
}
try(ByteArrayOutputStream bos =new ByteArrayOutputStream();) {
    byte[] info ="我们的开始,是很长的电影。".getBytes();
    bos.write(info, 0, info.length);
    //获取数据
    byte[] dest =bos.toByteArray();
    System.out.println(new String(dest));
} catch (IOException e) {
    e.printStackTrace();
}

管道流(了解)

public static void main(String[] args) throws IOException {
    final PipedOutputStream pipedOutputStream = new PipedOutputStream();
    final PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream);

    Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + "写数据");
                pipedOutputStream.write("烟花易冷 人事易分".getBytes(StandardCharsets.UTF_8));
                pipedOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    });

    Thread thread2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                byte[] flush =new byte[1024];
                int len =0;
                while(-1!=(len=pipedInputStream.read(flush))){
                    System.out.println(Thread.currentThread().getName() + "读数据");
                    System.out.println(new String(flush,0,len));
                }

                pipedInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    });
    thread1.start();
    thread2.start();
}

数据流(了解)

基本数据类型输入输出流是一个字节流,该流不仅可以读写字节和字符,还可以读写基本数据类型。

DataInputStream 提供了一系列可以读基本数据类型的方法

public static void main(String[] args) throws IOException {
    DataOutputStream das = new DataOutputStream(new FileOutputStream("/Users/fcp/test.txt"));
    das.writeByte(110);
    das.writeShort(120);
    das.writeInt(119);
    das.writeLong(12306L);
    das.writeFloat(3.14F);
    das.writeDouble(1.927);
    das.writeBoolean(true);
    das.writeChar('A');
    DataInputStream dis = new DataInputStream(new FileInputStream("/Users/fcp/test.txt"));
    byte b = dis.readByte() ;
    short s = dis.readShort() ;
    int i = dis.readInt();
    long l = dis.readLong() ;
    float f = dis.readFloat() ;
    double d = dis.readDouble() ;
    boolean bb = dis.readBoolean() ;
    char ch = dis.readChar() ;
    System.out.println(b + " " + s + " " + i + " " + l + " " + f + " " + d + " " + bb + " " + ch);
}

打印流(了解)

public static void main(String[] args) throws IOException {
    StringWriter buffer = new StringWriter();
    try (PrintWriter pw = new PrintWriter(buffer)) {
        pw.println("青石板街 回眸一笑你婉约");
    }
    System.out.println(buffer.toString());
}

转换流(了解)

序列化和反序列

对象流:把java创建的对象以字节的形式保存到磁盘上,但是需要被保存的对象都必须实现Serializable。

public class TestSerializable {
    public static void main(String[] args) {
        serializable();
        unSerializable();
    }

    public static void serializable() {
        Employee e = new Employee();
        e.name = "zhangsan";
        e.address = "beiqinglu";
        e.age = 20;
        try (FileOutputStream fileOut = new FileOutputStream("aa.txt");
             ObjectOutputStream out = new ObjectOutputStream(fileOut);) {
            // 创建序列化流对象
            // 写出对象
            out.writeObject(e);
            // 释放资源
            System.out.println("Serialized data is saved"); // 姓名,地址被序列化,年龄没有被序列化。
        } catch (IOException i) {
            i.printStackTrace();
        }
    }

    public static void unSerializable() {
        Employee e = null;
        try(FileInputStream fileIn = new FileInputStream("aa.txt");
            ObjectInputStream in = new ObjectInputStream(fileIn);) {
            // 读取一个对象
            e = (Employee) in.readObject();
            // 释放资源
        }catch(IOException i) {
            // 捕获其他异常
            i.printStackTrace();
            return;
        }catch(ClassNotFoundException c)  {
            // 捕获类找不到异常
            System.out.println("Employee class not found");
            c.printStackTrace();
            return;
        }
        // 无异常,直接打印输出
        System.out.println("Name: " + e.name); // zhangsan
        System.out.println("Address: " + e.address); // beiqinglu
        System.out.println("age: " + e.age); // 0
    }

}

class Employee implements java.io.Serializable {
    private static final long serialVersionUID = 1L;
    public String name;
    public transient String address;    
    public transient int age;

    public void addressCheck() {
        System.out.println("Address  check : " + name + " -- " + address);
    }
}

Properties的操作

创建一个文件:database.properties,然后填入以下内容

key1=value1
key2=value2
key3=value3
hello=world
mysql=localhost:3306/test
baidu=https://www.baidu.com

在代码里读取database.properties的内容

public class TestProperties {
    public static void main(String[] args) throws IOException {
        Properties hashmap = new Properties();
        hashmap.load(new FileInputStream("database.properties"));
        System.out.println(hashmap.get("key1"));
        for (Map.Entry<Object, Object> entry : hashmap.entrySet()) {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }
    }
}

作业:

歌词内容如下

Every night in my dreams

I see you I feel you

That is how I know you go on

Far across the distance

And spaces between us

You have come to show you go on

Near far

Wherever you are

I believe

That the heart does go on

Once more you open the door

And you're here in my heart

And my heart will go on and on

Love can touch us one time

And last for a lifetime

And never let go till we're gone

Love was when I loved you

One true time I hold to

In my life well always go on

Near far

Wherever you are

I believe

That the heart does go on

Once more you open the door

And you're here in my heart

And my heart will go on and on

you're here

There's nothing I fear

And I know

That my heart will go on

We'll stay forever this way

You are safe in my heart

And my heart will go on and on

​ \1. 将上面歌词内容存放到本地磁盘D 根目录,文件命名为 word.txt

​ \2. 选择合适的IO流读取word.txt文件的内容

​ \3. 统计每个单词出现的次数(单词忽略大小写)

​ \4. 如果出现组合单词如 you're按一个单词处理

​ \5. 将统计的结果存储到本地磁盘D根目录下的wordcount.txt文件

​ wordcount.txt每行数据个数如下

​ and 10个

​ konw 20个

public class Homework {
    public static void main(String[] args) {
        try(FileInputStream fis = new FileInputStream("word.txt");
            FileOutputStream fos = new FileOutputStream("wordcount.txt")) {
            //定义字节数组存储读取到的内容
            byte[] bytes = new byte[1024];//一次读取1KB
            //因为文件的内容可能大于1KB,所以我们需要循环读取多次
            StringBuilder sb = new StringBuilder();//用于存放获取到的所有内容
            while (fis.read(bytes) != -1) {
               sb.append(new String(bytes));
            }
            String content = sb.toString();
            //替换掉sb里的换行符
            content = content.replace("\r", " ");
            content = content.replace("\n", " ");
            content = content.replace("\r\n", " ");
            //把内容转成小写
            content = content.toLowerCase();
            //按空格切分字符串
            String[] contents = content.split(" ");
            HashMap<String, Integer> map = new HashMap<>();//用于统计每个单词出现的次数
            for (String word : contents) {
                if (map.containsKey(word)) {//如果key相同,取出value,让其加1,然后再存回去
                    map.put(word, map.get(word) + 1);
                } else {
                    map.put(word, 1);
                }
            }
            //把map的内容写到文件wordcount.txt里,每个key-value存做一行
            //遍历map
            for (Map.Entry<String,Integer> entry : map.entrySet()) {
                fos.write((entry.getKey() + " " + entry.getValue() + "个\n").getBytes());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}



//曾昭洋
public class HomeWork {
    static ConcurrentHashMap<String,Integer> map = new ConcurrentHashMap<>();
    public static void main(String[] args) {handleFile();}

    private static void handleFile() {
        try(BufferedReader rd =new BufferedReader(new FileReader("D:\\word.txt"));
            BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\wordcount.txt"))){
            String line;
            while ((line =rd.readLine()) != null){ // 把每行内容读出来,存在line里
                String[] wordGroup= line.toLowerCase().split(" "); // 把每行的内容先变成小写然后按空格分割,存到wordGroup里
                for (String word : wordGroup) {//word代表每个单词
                    if (!(map.containsKey(word))) map.put(word, 1);// 如果没出现过就存进去
                    else map.put(word, map.get(word) + 1);// 如果出现过就把value+1覆盖之前的数据
                }
            }
            for (ConcurrentHashMap.Entry<String,Integer> kvp : map.entrySet()) bw.write(String.format("%s  %s个\n",kvp.getKey(),kvp.getValue()));//按照我指定的format格式写到wordcount文件中
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

十四、网络编程

计算机网络

网络(Network):由若干节点(Node)和连接这些节点的链路(Link)组成

img

互联网:多个网络通过路由器连接组成更大的网络,即互联网,因此,互联网是“网络的网络”

img

因特网(Internet):世界上最大的互连网络

img

internet与Internet的区别

  • internet(互联网或互连网):泛指多个计算机网络互连而成的网络。在这些网络之间的通信协议可以是任意的。
  • Internet(因特网)则是一个专用名词,它指当前全球最大的、开放的、由众多网络互连而成的特定计算机网络,它采用TCP/IP协议族作为通信的规则,其前身是美国的ARPANET。

任意把几个计算机网络互连起来(不管采用什么协议),并能够相互通信,这样构成的是一个互连网(internet) ,而不是因特网(Internet)

因特网发展三个阶段

  1. 从单个网络ARPANET向互联网发展

    1. 1969年,第一个分组交换网络ARPANET;
    2. 70年代中期,研究多种网络之间的互连;
    3. 1983年,TCP/IP协议成为ARPANET的标准协议(因特网从此诞生)
  2. 逐步建成三级结构的因特网

    1. 1985年,NSF(美国国家科学基金会)围绕六个大型计算机中心建设NSFNET(主干网,地区网和校园网);
    2. 1990年,ARPANET任务完成,正式关闭;
    3. 1991年,美国政府将因特网主干网交给私营公司,并开始对接入因特网的单位收费;
  3. 逐步形成了多层次ISP结构的因特网

    1. 1993年,NSFNET逐渐被若干个商用因特网主干网替代,政府机构不在负责因特网运营,让各ISP来运营;
    2. 1994年,万维网WWW技术促使因特网迅猛发展;
    3. 1995年,NSFNET停止运作,因特网彻底商业化。

网络模型

OSI标准参考模型

OSI(Open System Interconnect),即开放式系统互联。

img

每层功能:

  • 应用层:应用层负责文件访问和管理、可靠运输服务、远程操作服务。(HTTP、FTP、SMTP)。
  • 表示层:表示层负责定义转换数据格式及加密,允许选择以二进制或ASCII格式传输。
  • 会话层:会话层负责使应用建立和维持会话,使通信在失效时继续恢复通信。(断点续传)。
  • 传输层:传输层负责是否选择差错恢复协议、数据流重用、错误顺序重排。(TCP、UDP)。
  • 网络层:网络层负责定义了能够标识所有网络节点的逻辑地址。(IP地址)。
  • 数据链路层:链路层在物理层上,通过规程或协议(差错控制)来控制传输数据的正确性。(MAC)。
  • 物理层:物理层为设备之间的数据通信提供传输信号和物理介质。(双绞线、光导纤维)。

TCP/IP模型

  • TCP/IP模型是因特网使用的参考模型,基于TCP/IP的参考模型将协议分成四个层次。
  • 该模型中最重要的两个协议是TCP和IP协议。

img

每层功能:

  • 应用层:应用层负责传送各种最终形态的数据,是直接与用户打交道的层,典型协议是HTTP、FTP等。
  • 传输层:传输层负责传送文本数据,主要协议是TCP、UDP协议。
  • 网络层:网络层负责分配地址和传送二进制数据,主要协议是IP协议。
  • 网络接口层:也叫链路层,接口层负责建立电路连接,是整个网络的物理基础,典型的协议包括以太网、ADSL等等。

OSI模型是理论上的国际标准,TCP/IP模型是事实上的国际标准。所以我们学习的是事实标准TCP/IP

TCP/IP协议族

在该协议族下,有许多的协议,但是通常都把他们统称为tcp/ip协议

img

以下是两个应用层的主机间进行通信,二者都使用FTP协议,下图列出了该过程所涉及到的所有协议。

img

在图右边,我们注意到应用程序通常是一个用户进程,而下三层则一般在(操作系统)内核中执行。尽管这不是必需的,但通常都是这样处理的,例如UNIX操作系统。

在图中,顶层与下三层之间还有另一个关键的不同之处。应用层关心的是应用程序的细节,而不是数据在网络中的传输活动。下三层对应用程序一无所知,但它们要处理所有的 通信细节。

数据在传输的过程中都需要封装,如下图

img

网络编程三要素

三要素分别是:ip地址、端口号、协议(tcp, udp)

IP地址

IP地址分为IPv4和IPv6,但在实际工作中几乎都是用IPv4,所以我们这里只介绍IPv4相关概念

ip地址:ip地址是给因特网上的每一台主机或路由器的每一个接口分配一个在全世界范围内唯一的32比特的标识符。

img

IPv4可分为以下几类

img

但在实际中我们使用到的只有A、B、C三类。

A类地址:0.0.0.0-127.255.255.255 B类地址:128.0.0.0-191.255.255.255 C类地址:192.0.0.0-223.255.255.255

InetAddress类

public class TestIP {
/**
 *
 *  //方式一:获取本机的InetAddress对象
 *         //通过本地的ip地址信息获取InetAddress对象
 *         InetAddress ia = InetAddress.getLocalHost();
 *         //InetAddress对象的常用方法
 *         //获取主机名(域名)
 *         String hostName = ia.getHostName();
 *         System.out.println(hostName);
 *         //获取ip地址
 *         String hostAddress = ia.getHostAddress();
 *         System.out.println(hostAddress);
 *  //方式二:根据指定的主机名获取InetAddress对象
 *         InetAddress ia = InetAddress.getByName("www.baidu.com");
 *         //InetAddress对象的常用方法
 *         String hostName = ia.getHostName();
 *         System.out.println(hostName);
 *         String hostAddress = ia.getHostAddress();
 *         System.out.println(hostAddress);
 *  //方式三:根据指定的ip地址获取InetAddress对象
 *          InetAddress ia = InetAddress.getByName("14.215.177.38");
 *          System.out.println(ia.getHostName());
 *          System.out.println(ia.getHostAddress());
 */
    public static void main(String[] args) throws UnknownHostException {
        //创建InetAddress对象
        //方式四:根据指定的主机名获取所有的InetAddress
        InetAddress[] ias = InetAddress.getAllByName("www.baidu.com");
        //使用lambda表达式遍历
        /*Arrays.stream(ias).forEach(ia ->{
            System.out.println(ia.getHostAddress());
            System.out.println(ia.getHostName());
        });*/
        //使用foreach循环遍历
        for (InetAddress ia : ias) {
            System.out.println(ia.getHostAddress());
            System.out.println(ia.getHostName());
        }
    }
}

端口号

img

常见的端口号

  • mysql端口号:3306
  • oracle端口号:1521
  • redis端口号:6379
  • tomcat端口号:8080
  • web服务器端口号:80
  • FTP服务器端口号:21

自己编程时用的端口号最好设置在1024~65535之间

TCP/UDP协议

  • UDP 和 TCP 是TCP/IP体系结构运输层中的两个重要协议

img

Socket编程

基于TCP

案例1 【掌握】

通过客户端向服务端发送信息
/**
 * 服务器
 */
public class ServerDemo02 {
    public static void main(String[] args) throws IOException {
        
        System.out.println("服务器启动.......");
        ServerSocket serverSocket = new ServerSocket(3305);//构造参数传端口号。
        //要让socket监听在3306这个端口号上。
        Socket socket = serverSocket.accept();//方法会阻塞等待。
        //获取输入流读取客户端内容
        InputStream inputStream = socket.getInputStream();
        byte[] bytes = new byte[1024];
        inputStream.read(bytes);
        System.out.println(new String(bytes));
        //关闭连接
        inputStream.close();
        socket.close();
        serverSocket.close();
    }
}
public class ClientDemo02 {
    public static void main(String[] args) throws IOException {
        //连接服务端,创建Socket对象
        Socket socket = new Socket("localhost", 3305);
        //想监听的端口号写内容
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("hello i am client".getBytes());
        //关闭连接
        outputStream.close();
        socket.close();
    }
}

让服务器不停止,一直接收多个客户端发的请求,代码如下:

public class ServerDemo02 {
    public static void main(String[] args) throws IOException {
        System.out.println("服务器启动.......");
        ServerSocket serverSocket = new ServerSocket(3305);//构造参数传端口号。
        int count = 10;
        //要让socket监听在3306这个端口号上。
        //要做到服务器不停止工作,只需要让阻塞的方法一直循环下去
        Socket socket = null;
        InputStream inputStream = null;
        while (count > 0) {
            socket = serverSocket.accept();//方法会阻塞等待。
            //获取输入流读取客户端内容
            inputStream = socket.getInputStream();
            byte[] bytes = new byte[1024];
            inputStream.read(bytes);
            System.out.println(new String(bytes));
            count--;
        }
        //关闭连接
        inputStream.close();
        socket.close();
        serverSocket.close();
    }
}

案例2 【掌握】

基于多线程实现客户端服务端通信
/**
 * 客户端程序
 */
public class ClientDemo02 {
    public static void main(String[] args) throws IOException {
        //使用循环模拟多个客户端发请求
        for (int i = 1; i < 20000; i++) {
            //连接服务端,创建Socket对象
            Socket socket = new Socket("localhost", 3305);
            //想监听的端口号写内容
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write(("hello i am client " + i).getBytes());
            //关闭连接
            outputStream.close();
            socket.close();
        }
    }
}
/**
 * 服务器
 */
public class ServerDemo02 {
    public static void main(String[] args) throws IOException, InterruptedException {
        //创建线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(16);
        System.out.println("服务器启动.......");
        ServerSocket serverSocket = new ServerSocket(3305);//构造参数传端口号。
        int count = 20000;
        //要让socket监听在3306这个端口号上。
        //要做到服务器不停止工作,只需要让阻塞的方法一直循环下去
        while (count > 0) {
            Socket socket = serverSocket.accept();
            //为了避免某个客户端任务处理时间过长,我们使用多线程来为每一个客户端任务创建一个单独的执行任务
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    //方法会阻塞等待。
                    try(InputStream inputStream = socket.getInputStream();) {
                        System.out.println(Thread.currentThread().getName() + "开始工作");
                        //获取输入流读取客户端内容
                        byte[] bytes = new byte[1024];
                        inputStream.read(bytes);
                        System.out.println(new String(bytes));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }
            });
            count--;
        }
        //关闭连接
        serverSocket.close();
    }
}

案例3 【掌握】

实现客户端文件上传功能,并从服务端向客户端发送数据
/**
 * 服务器
 */
public class ServerDemo02 {
    public static void main(String[] args) throws IOException, InterruptedException {
        //创建线程池
        System.out.println("服务器启动.......");
        ServerSocket serverSocket = new ServerSocket(3305);//构造参数传端口号。
        int count = 30;
        //要让socket监听在3306这个端口号上。
        //要做到服务器不停止工作,只需要让阻塞的方法一直循环下去
        Socket socket = null;
        InputStream inputStream = null;
        while (count > 0) {
            socket = serverSocket.accept();//方法会阻塞等待。
            //向客户端发送文件
            try(FileInputStream fis = new FileInputStream("/Users/fcp/Downloads/3306.mp3");
                OutputStream outputStream =socket.getOutputStream();) {
                byte[] bytes = new byte[1024];//每次读取文件1KB内容
                while (fis.read(bytes) != -1) {
                    outputStream.write(bytes);
                }
            }
            count--;
        }
        //关闭连接
        inputStream.close();
        socket.close();
        serverSocket.close();
    }
}

public class ClientDemo03 {
    public static void main(String[] args) throws IOException {
        //连接服务端,创建Socket对象
        Socket socket = new Socket("172.171.3.37", 3305);
        //接收服务器写的文件内容
        try(InputStream inputStream = socket.getInputStream();
            FileOutputStream fos = new FileOutputStream("jay.mp3");) {
            byte[] bytes = new byte[1024];
            while (inputStream.read(bytes) != -1) {
                fos.write(bytes);
            }
        }
        socket.close();
    }
}

使用Socket实现http服务器

/**
 * 服务器
 */
public class ServerDemo03 {
    public static void main(String[] args) throws IOException {
        //创建服务端socket通信对象
        System.out.println("服务器启动.......");
        ServerSocket serverSocket = new ServerSocket(3305);//构造参数传端口号。
        Socket socket = serverSocket.accept();
        OutputStream outputStream = socket.getOutputStream();
        PrintWriter out = new PrintWriter(outputStream);
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("HTTP/1.1").append(" ").append(200).append(" ")
                .append("ok").append("\n");
        //stringBuilder.append("Content-Length").append(":").append("Hello world!".getBytes()).append("\n");
        stringBuilder.append("Content-Type").append(":")
        .append("text/html;charset=UTF-8").append("\n");
        stringBuilder.append("\n");
        //stringBuilder.append("欢迎使用Apache Tomcat");
        stringBuilder.append("<h1 style='color:red;'>下载音乐</h>");
        stringBuilder.append("<a href='/download'><button></button></a>");
        out.print(stringBuilder.toString());
        out.flush();
        //关闭连接
        /*inputStream.close();
        socket.close();
        serverSocket.close();*/
    }

基于UDP

案例1 【掌握】

发送端发送数据到接收端
public class Send {
    public static void main(String[] args) throws  Exception {
        //1、基于UDP协议创建Socket对象   无需指定端口号ip地址
        DatagramSocket ds = new DatagramSocket();
        //2、创建数据包对象
        byte[] buf = "你好接收端".getBytes();
        DatagramPacket dp = new DatagramPacket(buf,buf.length, InetAddress.getByName("192.168.73.210"),9999);
        //3、发送数据包
        ds.send(dp);
        //4、释放资源
        ds.close();
    }
}
public class Receive {
    public static void main(String[] args) throws Exception {
        //1、基于UDP协议创建Socket对象
        DatagramSocket ds = new DatagramSocket(9999);
        //2、创建数据包对象
        byte[] buf = new byte[1024];
        DatagramPacket dp = new DatagramPacket(buf,buf.length);
        //3、接受数据包  receive阻塞式的方法
        ds.receive(dp);

        //4、解析数据包的数据
        //从数据包中获取数据
        byte[] data = dp.getData();
        //从数据包中获取InetAddress对象
        InetAddress inetAddress = dp.getAddress();
        //从数据包中获取端口号
        int port = dp.getPort();
        //获取数据包的数据字节数长度
        int length = dp.getLength();


        String str = new String(data,0,length);
        System.out.println("ip:"+inetAddress.getHostAddress());
        System.out.println("port:"+port);
        System.out.println("data:"+str);

        //5、释放资源
        ds.close();
    }
}

案例2【了解】

使用UDP协议向飞秋发送数据
public class Send {
    public static void main(String[] args) throws Exception {
        //分析:只要搞清楚飞秋的数据包的格式
        //1:100:主机名:昵称:32:hello,飞秋
        //1、创建Socket对象
        DatagramSocket ds = new DatagramSocket();
        //2、创数据包对象
        byte[] buf = "1:100:主机名:昵称:32:hello,飞秋".getBytes("GBK");
        DatagramPacket dp = new DatagramPacket(buf,buf.length, InetAddress.getByName("192.168.73.210"),2425);
        //3、发送数据包
        ds.send(dp);
        //4、释放资源
        ds.close();
    }
}

面试题

TCP三次握手

TCP 连接的建立采用客户服务器方式。

主动发起连接建立的应用进程都是客户端

服务端都是被动等待连接。

  1. 最初两端的TCP进程都处于关闭状态一开始,TCP服务器进程首先创建传输控制块,用来存储TCP连接中的一些重要信息。例如TCP连接表、指向发送和接收缓存的指针、指向重传队列的指针,当前的发送和接收序号等之后,就准备接受TCP客户端进程的连接请求,此时,TCP服务器进程就进入监听状态,等待TCP客户端进程的连接请求
  2. TCP服务器进程是被动等待来自TCP客户端进程的连接请求,因此成为被动打开连接
  3. TCP客户进程也是首先创建传输控制块,由于TCP连接建立是由TCP客户端主动发起的,因此称为主动打开连接。然后,在打算建立TCP连接时,向TCP服务器进程发送TCP连接请求报文段,并进入同步已发送状态TCP连接请求报文段首部中同步位SYN被设置为1,表明这是一个TCP连接请求报文段序号字段seq被设置了一个初始值x,作为TCP客户端进程所选择的初始序号,注意:TCP规定SYN被设置为1的报文段不能携带数据,但要消耗掉一个序号
  4. TCP服务器进程收到TCP连接请求报文段后,如果同意建立连接,则向TCP客户进程发送TCP连接请求确认报文段,并进入同步已接收状态。

    1. TCP连接请求确认报文段首部中:
    2. ①同步位SYN和确认位ACK都设置为1,表明这是一个TCP连接请求确认报文段序;
    3. ②号字段seq被设置了一个初始值y,作为TCP服务器进程所选择的初始序号;
    4. ③确认号字段ack的值被设置成了x+1,这是对TCP客户进程所选择的初始序号(seq)的确认请。

注意:这个报文段也不能携带数据,因为它是SYN被设置为1的报文段,但同样要消耗掉一个序号

  1. TCP客户进程收到TCP连接请求确认报文段后,还要向TCP服务器进程发送一个普通的TCP确认报文段,并进入连接已连接状态。
  • 普通的TCP确认报文段首部中
  • ①确认位ACK被设置为1,表明这是一个普通的TCP确认报文段;
  • ②序号字段seq被设置为x+1,这是因为TCP客户进程发送的第一个TCP报文段的序号为x,所以TCP客户进程发送的第二个报文段的序号为x+1;
  • ③确认号字段ack被设置为y+1,这是对TCP服务器进程所选择的初始序号的确认。

注意:TCP规定普通的TCP确认报文段可以携带数据,但如果不携带数据,则不消耗序号

img

TCP四次挥手

img

为什么要三次握手

tcp之所以设计3次握手的原因并不是因为服务器连接资源浪费的问题。当然三次握手可以避免这种情况,但是当时设计的目的不是这个,而是为了告诉双方序列号。tcp是全双工可靠的传输协议。全双工意味着双方能够同时向对方发送数据。可靠意味着我发送的数据必须确认对方完整收到了。tcp是通过序列号来保证这两种性质的,比如发送端发送一个10字节的包,接受端回复一个对方序列号+10个字节的包,告诉对方你的这个包我收到了,并且是10字节。三次握手就是互换序列号的一次过程,2次则只能保证一方的序列号对方收到了了。4次则多余了。

十五、反射与注解

从本章开始,我们来探讨Java中的一些动态特性,包括反射、注解、动态代理、类加载器等。利用这些特性,可以优雅地实现一些灵活通用的功能,它们经常用于各种框架、库和系统程序中,比如:

  1. Jackson,利用反射和注解实现了通用的序列化机制。
  2. 有多种库(如Spring MVC、Jersey)用于处理Web请求,利用反射和注解,能方便地将用户的请求参数和内容转换为Java对象,将Java对象转变为响应内容。
  3. 有多种库(如Spring、Guice)利用这些特性实现了对象管理容器,方便程序员管理对象的生命周期以及其中复杂的依赖关系。
  4. 应用服务器(如Tomcat)利用类加载器实现不同应用之间的隔离,JSP技术利用类加载器实现修改代码不用重启就能生效的特性。
  5. 面向切面的编程AOP(Aspect Oriented Programming)将编程中通用的关注点(如日志记录、安全检查等)与业务的主体逻辑相分离,减少冗余代码,提高程序的可维护性,AOP需要依赖上面的这些特性来实现。

14.1、反射

在一般操作数据的时候,我们都是知道具体要操作数据的数据类型的。比如:

1)根据类型使用new创建对象。

2)根据类型定义变量,类型可能是基本类型、类、接口或数组。

3)将特定类型的对象传递给方法。

4)根据类型访问对象的属性,调用对象的方法。

编译器也是根据类型进行代码的检查编译的。

反射不一样,它是在运行时,而非编译时,动态获取类型的信息,比如接口信息、成员信息、方法信息、构造方法信息等,根据这些动态获取到的信息创建对象、访问/修改成员、调用方法等。

下面分为两个部分来学习反射技术

  1. 介绍Class类的使用
  2. 通过示例演示Class的完整使用

什么是类对象

类的对象:基于某个类 new 出来的对象,也称为实例对象。

类对象:类加载的产物,封装了一个类的所有信息(类名、父类、接口、属性、方法、构造方法) 。

注意:每个类加载到内存都会生成一个唯一的类对象。

获取类对象

  • 如何获取Class对象,一共有三种方式获取类对象
public static  void test01(){
    //1、通过对象的getClass方法
    //Student stu = new Student();
    //Class c = stu.getClass();
    //class com.qf.reflect.Student  全的限定名(包名+类名)
    //System.out.println(c);

    //2、通过类的class属性
    //Class c = Student.class;
    //class com.qf.reflect.Student
    //System.out.println(c);

    //3、通过Class类的forName方法  参数:全限定名  (此方法的作用:1、触发类加载 2、获取类对象)
    try {
        Class c = Class.forName("com.qf.reflect.Student");
        System.out.println(c);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

类对象常用的方法

  • Class对象的常用方法
  • 调用newInstance方法需要注意:
public static void test02(){
    try {
        //1、获取类对象
        Class c = Class.forName("com.qf.reflect.Student");
        //获取类的名称(全限定名)
        System.out.println(c.getName());
        //获取类的名称
        System.out.println(c.getSimpleName());
        //获取类加载器(我们可以使用它获取src下的资源配置文件)
        System.out.println(c.getClassLoader());
        //获取父类的类对象
        Class superclass = c.getSuperclass();
        System.out.println(superclass);
        //获取实现接口的类对象
        Class[] interfaces = c.getInterfaces();
        System.out.println(Arrays.toString(interfaces));
        //获取Package对象
        Package aPackage = c.getPackage();
        System.out.println(aPackage);
        //通过类对象获取类的对象(注意:1、必须要保留无参构造2、构造方法必须使用public修饰)
        Object o = c.newInstance();
        System.out.println(o);
    } catch (Exception e) {
        e.printStackTrace();
    } 
}

Field类

Field类表示类对象中的属性

获取它的目的:赋值和取值

public static void test04(){
    //3、通过Class类的forName方法  参数:全限定名  (此方法的作用:1、触发类加载 2、获取类对象)
//此时引用c3就代表了一个Student类,我们需要通过这个类来创建对象。
Class c3 = Class.forName("oop.Student");
//创建对象
Object instance = c3.getDeclaredConstructor().newInstance();
//获取指定公开的属性
Field field = c3.getField("age");
System.out.println(field);
System.out.println(field.getInt(instance));//获取instance这个对象里的field代表的属性的值
//对属性进行赋值, 把对象instance里的field代表的属性的值设置为102
field.set(instance, 102);
//获取属性中的值
System.out.println(field.get(instance));

//获取所有公开的属性(包括父类的)
Field[] fields = c3.getFields();
System.out.println(Arrays.toString(fields));

//获取执行的私有属性
Field field1 = c3.getDeclaredField("marriage");
System.out.println(field1);
System.out.println(((Student)instance).isMarriage());
field1.setAccessible(true);//设置之后才能对private修饰的属性进行修改。
field1.set(instance, true);
System.out.println(field1.get(instance));

//获取所有的属性(包括私有的)
Field[] fieldss = c3.getDeclaredFields();
System.out.println(Arrays.toString(fieldss));
//获取public修饰的属性列表  
Field[] fieldList = c3.getFields();
System.out.println(Arrays.toString(fieldList));
}

Constructor类

Constructor类表示类对象中的构造方法

获取它的目的,创建对象

public static void main(String[] args) throws Exception { //1、获取类对象 Class c = Class.forName("oop.Student");//c代表某个类的Class对象 //获取类Stuent里public修饰的构造方法 Constructor[] constructors = c.getConstructors(); System.out.println(Arrays.toString(constructors)); //获取全部构造方法,不区分访问修饰符 Constructor[] declaredConstructors = c.getDeclaredConstructors(); System.out.println(Arrays.toString(declaredConstructors)); //如果一个类里的构造方法被private修饰后,那么就不能直接使用new关键字来创建对象 //但是可以通过反射获取该private修饰的构造器,并且设置访问属性为true,即可访问私有构造方法 //同理,私有方法和私有属性也可以通过设置改访问属性后访问到。 Constructor privateConstructor = c.getDeclaredConstructor(); privateConstructor.setAccessible(true);//改法可以提升反射访问效率 Object instance = privateConstructor.newInstance(); System.out.println(instance); //获取带参数的构造方法 Constructor declaredConstructor = c.getDeclaredConstructor(String.class); declaredConstructor.setAccessible(true); Object obj = declaredConstructor.newInstance("小明"); System.out.println(((Student)obj).name);//该方式是通过写具体对象,实际场景不推荐这种写法。 //应该使用以下反射获取属性 Field field = c.getField("name"); System.out.println(field.get(obj)); }

Method类

Method类表示类对象的方法

获取它的目的,调用它并获取方法的返回值(如果方法没有返回值则返回null)

public static void test05(){
    try {
        //1、获取类对象
        Class c = Class.forName("oop.Student");//c代表某个类的Class对象
        Object o = c.getDeclaredConstructor().newInstance();//具体类的对象
        //获取指定公开的方法
        Method method = c.getMethod("show");//获取Student类里的show无参数方法
        //利用反射调用方法,并接收返回值
        method.invoke(o);//会去执行show这个方法。


        //获取所有的公开的方法(包括父类的公开的方法)
        //Method[] methods = c.getMethods();
        //System.out.println(Arrays.toString(methods));

        //获取指定的私有方法
        //Method method = c.getDeclaredMethod("print");
        //method.setAccessible(true);
        //利用反射调用方法,并接收返回值(如果方法没有返回值。那么invoke方法就返回null)
        //Object result = method.invoke(o);
        //System.out.println(result);

        //获取本类中所有的方法(包括私有的)
        Method[] methods = c.getDeclaredMethods();
        System.out.println(Arrays.toString(methods));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

综合练习

public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
    //创建服务端socket通信对象
    System.out.println("服务器启动.......");
    ServerSocket serverSocket = new ServerSocket(3305);//构造参数传端口号。
    Socket socket = serverSocket.accept();//程序阻塞等待客户端连接
    InputStream inputStream = socket.getInputStream();//获取输入流读取浏览器的请求数据
    byte[] bytes = new byte[1024*8];
    inputStream.read(bytes);
    String content = new String(bytes);//把读取的数据转成字符串
    //把请求内容里的换行替换为空格
    content = content.replace("\r", " ");
    content = content.replace("\n", " ");
    content = content.replace("\r\n", " ");
    //把请求内容按照空格切割,切割后数组里的第二个元素就是路径名
    String path = content.split(" ")[1];
    System.out.println(path);
    //根据路径名到配置文件里找到对应的Servlet也就是java文件的全限定名
    Properties properties = new Properties();
    properties.load(new FileReader("web.properties"));
    String className = (String) properties.get(path);
    System.out.println("classname : " + className);

    //从配置文件读到全限定名后就可以使用反射创建对象
    Class<?> aClass = Class.forName(className);
    Object instance = aClass.getDeclaredConstructor().newInstance();

    //通过反射调用路径对应的java文件的service方法执行用户的具体请求操作
    Method method = aClass.getMethod("service");
    method.invoke(instance);

    OutputStream outputStream = socket.getOutputStream();
    PrintWriter out = new PrintWriter(outputStream);
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append("HTTP/1.1").append(" ").append(200).append(" ")
            .append("ok").append("\n");
    //stringBuilder.append("Content-Length").append(":").append("Hello world!".getBytes()).append("\n");
    stringBuilder.append("Content-Type").append(":").append("text/html;charset=UTF-8").append("\n");
    stringBuilder.append("\n");
    //stringBuilder.append("欢迎使用Apache Tomcat");
    stringBuilder.append("<h1 style='color:red;'>欢迎使用Tomcat</h>");
    //stringBuilder.append("<img src=''>下载音乐</h>");
    //stringBuilder.append("<a href='172.171.3.37:3305'><button></button></a>");
    out.print(stringBuilder.toString());
    out.flush();
    //关闭连接
    /*inputStream.close();
    socket.close();
    serverSocket.close();*/
}

web.properties内容

/login=socket.servlet.LoginServlet

示例

介绍了一堆Class的方法,他们有什么用呢?我们利用反射实现一个简单的通用序列化/反序列化功能,主要通过两个方法实现

public static String serialization(Object obj);//序列化
public static Object deserialization(String str);//反序列化
{
    "age":12,
    "name":"小明"
}

serialization将对象obj转换为字符串,deserialization将字符串转换为对象。为简单起见,我们只支持最简单的类,即有默认构造方法,成员类型只有基本类型、包装类或String。

测试案例

public static void main(String[] args) {
        Student student1 = new Student("jay", 18, 2004);
        String studentStr = serialization(student1);
        System.out.println(studentStr);
        Student student2 = (Student) deserialization(studentStr);
        System.out.println(student2);
}

最后对象引用zhangsan2的输出内容要和zhangsan一致。

代码实现

实体类Student

public class Student {
    private String username;
    private Integer age;
    private Integer birthYear;

    public Student(){}

    public Student(String username, Integer age, Integer birthYear) {
        this.username = username;
        this.age = age;
        this.birthYear = birthYear;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Integer getBirthYear() {
        return birthYear;
    }

    public void setBirthYear(Integer birthYear) {
        this.birthYear = birthYear;
    }

    @Override
    public String toString() {
        return "Student{" +
                "username='" + username + '\'' +
                ", age=" + age +
                ", birthYear=" + birthYear +
                '}';
    }
}

功能实现类

public class Demo01 {
    public static void main(String[] args) {
        Student student = new Student("Jay", 18, 2004);
        String studentStr = serialization(student);
        System.out.println(studentStr);
        Student deserializationObj = (Student) deserialization(studentStr);
        System.out.println(deserializationObj);
    }

    public static String serialization(Object obj) {
        Class<?> clazz = obj.getClass();
        String clazzName = clazz.getName();
        StringBuilder sb = new StringBuilder(clazzName);
        sb.append("\n{");
        Field[] declaredFields = clazz.getDeclaredFields();
        sb.append("\n");
        for (Field field : declaredFields) {
            if (!field.isAccessible())
                field.setAccessible(true);
            try {
                sb.append("\t").append("\"");
                sb.append(field.getName()).append("\":").append("\"" + field.get(obj) + "\",");
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            sb.append("\n");
        }
        String s = sb.toString();
        s = s.substring(0, s.length()-2);
        s = s + "\n}";
        return s;
    }

    public static Object deserialization(String str) {
        try {
            String[] lines = str.split("\n");
            if(lines.length < 1) {
                throw new IllegalArgumentException(str);
            }
            Class<?> cls = Class.forName(lines[0]);
            Object obj = cls.getDeclaredConstructor().newInstance();
            if(lines.length > 1) {
                for(int i = 2; i < lines.length-1; i++) {
                    String[] map = lines[i].split(":");
                    if(map.length != 2) {
                        throw new IllegalArgumentException(lines[i]);
                    }
                    Field f = cls.getDeclaredField(map[0]);
                    if(!f.isAccessible()){
                        f.setAccessible(true);
                    }
                    //setFieldValue(f, obj, fv[1]);
                    f.set(obj, map[1]);
                }
            }
            return obj;
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static void setFieldValue(Field f, Object obj, String value)
            throws Exception {
        Class<?> type = f.getType();
        if(type == int.class) {
            f.setInt(obj, Integer.parseInt(value));
        } else if(type == byte.class) {
            f.setByte(obj, Byte.parseByte(value));
        } else if(type == short.class) {
            f.setShort(obj, Short.parseShort(value));
        } else if(type == long.class) {
            f.setLong(obj, Long.parseLong(value));
        } else if(type == float.class) {
            f.setFloat(obj, Float.parseFloat(value));
        } else if(type == double.class) {
            f.setDouble(obj, Double.parseDouble(value));
        } else if(type == char.class) {
            f.setChar(obj, value.charAt(0));
        } else if(type == boolean.class) {
            f.setBoolean(obj, Boolean.parseBoolean(value));
        } else if(type == String.class) {
            f.set(obj, value);
        } else {
            Constructor<?> ctor = type.getConstructor(
                    new Class[] { String.class });
            f.set(obj, ctor.newInstance(value));
        }
    }
}

实战练习

使用反射实现把对象转成Json字符串,把Json字符串转成对象。

类似Alibaba的FastJson以及google的Gson。

public class Demo01 {
    public static void main(String[] args) throws Exception{
        //创建对象
        Student1 student = new Student1("小明", "贵州黔西南", 25, 1998);
        //调用一个方法实现把对象转成json字符串
        String json = toJson(student);
        System.out.println(json);
        toObject(json);
    }

    /**
     * {
     *     "age":12,
     *     "name":"小明"
     * }
     * @param obj
     */
    public static String toJson(Object obj) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        StringBuilder jsonBuffer = new StringBuilder();
        //获取该对象的Class对象
        Class<?> clazz = obj.getClass();
        jsonBuffer.append(clazz.getName());
        jsonBuffer.append("\n{\n");
        //创建对象
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            //因为属性通常都是private修饰,所以需要设置访问权限
            field.setAccessible(true);
            //获取变量名
            String variableName = field.getName();
            //获取变量的值
            Object variableValue = field.get(obj);
            jsonBuffer.append("\"")
                    .append(variableName).append("\":");
            //判断,如果属性不是字符就不要加双引号
            Class<?> type = field.getType();
            if (type == int.class) {
                jsonBuffer.append(variableValue)
                        .append(",\n");
            } else {
                jsonBuffer.append("\"").append(variableValue)
                        .append("\",\n");
            }
        }
        String json = jsonBuffer.toString();
        json = json.substring(0, json.length()-2);
        json = json + "\n}";
        return json;
    }
    public static Object toObject(String json) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        //获取字符串里的类权限定名
        String[] arr = json.split("\n");
        String className = arr[0];
        Class<?> clazz = Class.forName(className);
        Object instance = clazz.getDeclaredConstructor().newInstance();
        for (int i = 2; i < arr.length-1; i++) {
            String variVal = arr[i].split(",")[0];
            String[] split = variVal.split(":");
            String atrribute  = split[0];
            atrribute = atrribute.substring(1, atrribute.length()-1);

            Field field = clazz.getDeclaredField(atrribute);
            field.setAccessible(true);//访问私有属性
            Class<?> type = field.getType();
            if (type == String.class) {
                String value = split[1];
                value = value.substring(1, value.length()-1);
                field.set(instance, value);
            } else {
                field.set(instance, Integer.parseInt(split[1]));
            }
        }
        System.out.println(instance);
        return null;
    }
}

class Student1 {
    private String username;
    private String address;
    private int age;
    private int birthYear;

    public Student1(){}

    public Student1(String username, String address, int age, int birthYear) {
        this.username = username;
        this.address = address;
        this.age = age;
        this.birthYear = birthYear;
    }

    @Override
    public String toString() {
        return "Student1{" +
                "username='" + username + '\'' +
                ", address='" + address + '\'' +
                ", age=" + age +
                ", birthYear=" + birthYear +
                '}';
    }
}

14.2、注解

概念

注解(Annotation):是一种引用数据类型。编译之后也会生成class文件

自定义注解语法格式:

[修饰符] @interface 注解名{
}

注解可以用在类、属性、方法上。

JDK内置了哪些注解?

  • Java中目前有 5 种标准注解。
  • 5 种元注解,元注解用于注解其他的注解。

标准注解

  • @Override :表示当前的方法定义将覆盖基类的方法。如果你不小心拼写错误,或者方法签名被错误拼写的时候,编译器就会发出错误提示。
  • @Deprecated :如果使用该注解的元素被调用,编译器就会发出警告信息。
  • @SuppressWarnings :关闭不当的编译器警告信息。
  • @SafeVarargs :在 Java 7 中加入用于禁止对具有泛型varargs参数的方法或构造函数的调用方发出警告。
  • @FunctionalInterface :Java 8 中加入用于表示类型声明为函数式接口

元注解

  • @Target:表示注解可以用于哪些地方。可能的 ElementType 参数包括:

CONSTRUCTOR :构造器的声明

FIELD :字段声明(包括 enum 实例)

LOCAL_VARIABLE :局部变量声明

METHOD :方法声明

PACKAGE :包声明

PARAMETER :参数声明

TYPE :类、接口(包括注解类型)或者 enum 声明

  • @Retention:表示注解信息保存的时长。可选的 RetentionPolicy 参数包括:

SOURCE :表示注解只被保留在java源文件中

CLASS :注解在 class 文件中可用,但是会被 VM 丢弃。

RUNTIME :VM 将在运行期也保留注解,因此可以通过反射机制读取注解的信息。

  • @Documented:将此注解保存在 Javadoc 中
  • @Inherited :允许子类继承父类的注解
  • @Repeatable:允许一个注解可以被使用一次或者多次(Java 8)。

自定义注解

我们自定义一个@Test注解来对testExecute()进行注解。注解的定义看起来很像接口的定义。事实上,它们和其他 Java 接口一样,也会被编译成 class 文件。

定义注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Demo01 {
}

除了 @ 符号之外,@Demo01 的定义看起来更像一个空接口。注解的定义也需要一些元注解(meta-annoation),比如@Target和@Retention。@Target 定义你的注解可以应用在哪里(例如是方法还是字段)。@Retention定义了注解在哪里可用,在源代码中(SOURCE),class文件(CLASS)中或者是在运行时(RUNTIME)。

使用注解

public class Testable {
    public void execute(){
        System.out.println("Executing...");
    }

    @Test
    public void testExecute() {
        execute();
    }
}

不包含任何元素的注解称为标记注解(marker annotation),例如上例中的 @Demo01 就是标记注解。

可以为注解定义一些参数,定义的方式是在注解内定义一些方法。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Label {
    String value() default "";
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Format {
    String pattern() default "yyyy-MM-dd HH:mm:ss";
    String timezone() default "GMT+8";
}

实例演示

public class COVID_19 {
    @Label("名称")
    private String name;

    @Label("出生日期")
    @Format(pattern = "yyyy/MM/dd")
    private Date born;

    public COVID_19() {
    }

    public COVID_19(String name, Date born) {
        this.name = name;
        this.born = born;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Date getBorn() {
        return born;
    }

    public void setBorn(Date born) {
        this.born = born;
    }

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

    private static String format(Object obj) {
        try {
            Class<?> clazz = obj.getClass();
            StringBuilder sb = new StringBuilder();
            Field[] declaredFields = clazz.getDeclaredFields();
            for (Field field : declaredFields) {
                if (!field.isAccessible())
                    field.setAccessible(true);
                Label label = field.getAnnotation(Label.class);
                String name = label != null ? label.value() : field.getName();
                Object value = field.get(obj);
                if (value != null && field.getType() == Date.class) {
                    value = formatDate(field, value);
                }
                sb.append(name + ": " + value + "\n");
            }
            return sb.toString();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    private static Object formatDate(Field f, Object value) {
        Format format = f.getAnnotation(Format.class);
        if(format != null) {
            SimpleDateFormat sdf = new SimpleDateFormat(format.pattern());
            sdf.setTimeZone(TimeZone.getTimeZone(format.timezone()));
            return sdf.format(value);
        }
        return value;
    }
    
    public static void main(String[] args) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        try {
            COVID_19 covid_19 = new COVID_19("新冠病毒", sdf.parse("2020-01-01"));
            System.out.println(covid_19);
            System.out.println(format(covid_19));
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}

案例练习:实现一个依赖注入功能。

自定义注解MiniInject

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MiniInject {
}

定义一个类LoginService

public class LoginService {
    public void login() {
        System.out.println("test mini inject function");
    }
}

定义一个类LoginController,并且通过注解@MiniInject把对象LoginService注入到属性loginService上

public class LoginController {

    @MiniInject
    private LoginService loginService;

    public void login() {
        loginService.login();
    }
}

自定义容器类来负责生成客户代码需要的对象

public class IocContainer {
    public static<T> T getInstance(Class<T> cls) {
        try {
            //创建实例
            T instance = cls.newInstance();
            //获取类里的属性列表,目的是为了检查属性是否又被注解修饰。
            Field[] fields = cls.getDeclaredFields();
            for (Field field : fields) {
                //检查属性是否被注解MiniInject修饰
                if (field.isAnnotationPresent(MiniInject.class)) {
                    if (!field.isAccessible())
                        field.setAccessible(true);
                    //如果被注解MiniInject修饰,则注入依赖的对象
                    Class<?> fieldCls = field.getType();
                    //这里会递归调用getInstance方法,可以解决嵌套注入
                    field.set(instance, getInstance(fieldCls));
                }
            }
            return instance;
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }
}

测试

public class TestMiniInject {
    public static void main(String[] args) {
        LoginController instance = IocContainer.getInstance(LoginController.class);
        instance.login();
    }
}

14.2.3 实战练习(http服务器)

基于注解实现MiniHttp服务器

WebServlet注解类

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface WebServlet {
    String value() default "";
}

html页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <style>
        input {
            width: 200px;
            height: 30px;
            border-radius: 10px;
        }
        .container {
            width: 800px;
            margin-left: auto;
            margin-right: auto;
            margin-top: auto;
            margin-bottom: auto;
        }
    </style>
</head>
<body style="background-color: cornflowerblue">
    <div class="container">
        <label>用户名:</label>
        <input type="text" name="username" placeholder="username"><br><br>
        <label>密码:</label>
        <input type="password" name="username" placeholder="password"><br><br>
        <label>确认密码:</label>
        <input type="password" name="username" placeholder="......."><br><br>
        <label>邮箱:</label>
        <input type="email" name="username" placeholder="eamil"><br><br>
        <label>电话:</label>
        <input type="phone" name="username" placeholder="phone">
    </div>
</body>
</html>

服务器类

public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
    //创建服务端socket通信对象
    System.out.println("服务器启动.......");
    ServerSocket serverSocket = new ServerSocket(80);//构造参数传端口号。
    while (true) {
        Socket socket = serverSocket.accept();//程序阻塞等待客户端连接
        service(socket);
    }
}


public static void service(Socket socket) throws IOException, ClassNotFoundException {
    InputStream inputStream = socket.getInputStream();//获取输入流读取浏览器的请求数据
    InputStreamReader isr = new InputStreamReader(inputStream);//转换流
    BufferedReader br = new BufferedReader(isr);
    Request request = new Request();//创建对象

    int count  = 1;
    HashMap<String, String> header = new HashMap<>();
    for (int i = 0; i < 13; i++) {
        String b = br.readLine();
        if (b == null || b.equals("")) break;
        if (count == 1) {
            String[] requests = b.split(" ");
            //初始化对象里的属性
            request.setMethod(requests[0]);
            request.setUri(requests[1]);
            request.setVersion(requests[2]);
        } else {
            String[] split = b.split(":");
            if (split != null && split.length ==2) {
                //因为浏览器请求的header头部是按照key:value的形式存放,所以我们可以使用集合的Hasmap存储这部分内容
                header.put(split[0], split[1].trim());
            }
        }
        count++;
    }
    //把请求内容按照空格切割,切割后数组里的第二个元素就是路径名
    request.setHeaders(header);
    br.close();
    isr.close();
    inputStream.close();
    toDoList(request, socket);
}

private static void toDoList(Request request, Socket socket) throws ClassNotFoundException, IOException {
    //获取java文件的class路径
    URL url = MiniHttpServer.class.getResource("./");
    String classFilePath = url.toString().split(":")[1];
    String[] split = classFilePath.split("/");
    classFilePath = split[split.length-1];
    classFilePath = classFilePath + ".servlet";
    System.out.println("classname : " + classFilePath);
    //获取某个路径下的所有文件,使用File类
    File file = new File(url.toString().split(":")[1] + "servlet/");// /Users/fcp/food/java/java2202/out/production/javase/socket/servlet/
    String s = null;
    //首先判断该file是否存在
    if(file.exists()) {
        File[] files = file.listFiles();
        for (File f : files) {
            if (f.getName().endsWith("Servlet.class")) {//只使用servlet文件
                //通过字符串处理拼接类的全限定名
                String classFileName = classFilePath;
                classFileName = classFileName + "." + f.getName();
                classFileName = classFileName.substring(0, classFileName.length()-6);
                //通过反射获取该文件的Class对象。Class.forName("socket.servlet.xxxx");
                Class<?> clazz = Class.forName(classFileName);
                //查看该对象上的注解是否是WebServlet
                if (clazz.isAnnotationPresent(WebServlet.class)) {
                    WebServlet annotation = clazz.getAnnotation(WebServlet.class);
                    //获取注解的value
                    String value = annotation.value();
                    //判断该value是否和前端传入的地址相同
                    if (value.equals(request.getUri())) {
                        //通过反射创建该servlet对象,然后调用该对象的service方法
                       /* Object instance = clazz.getDeclaredConstructor().newInstance();
                        Method method = clazz.getDeclaredMethod("service");
                        method.invoke(instance);*/
                        //让服务器返回一个登录的html页面  login.html
                        //使用IO流读取html文件内容
                        FileInputStream fis = new FileInputStream("/Users/fcp/food/java/java2202/javase/src/html/login.html");
                        byte[] bytes1 = new byte[1024*8];
                        fis.read(bytes1);
                        s = new String(bytes1);
                    }
                }
            }
        }
    }
    response(request, socket, s);
}

private static void response(Request request, Socket socket, String content) throws IOException {
    OutputStream outputStream = socket.getOutputStream();
    PrintWriter out = new PrintWriter(outputStream);
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append(request.getVersion()).append(" ").append(200).append(" ")
            .append("ok").append("\n");
    //stringBuilder.append("Content-Length").append(":").append("Hello world!".getBytes()).append("\n");
    stringBuilder.append("Content-Type").append(":").append("text/html;charset=UTF-8").append("\n");
    stringBuilder.append("\n");
    stringBuilder.append(content);
    out.print(stringBuilder.toString());
    out.flush();
    //关闭连接
    socket.close();
}

Request类

public class Request {
    /**
     * 请求方法 GET/POST/PUT/DELETE/OPTION...
     */
    private String method;
    /**
     * 请求的uri
     */
    private String uri;
    /**
     * http版本
     */
    private String version;
    /**
     * 请求头
     */
    private Map<String, String> headers;
    /**
     * 请求参数相关
     */
    private String message;
}

十六、函数式编程

介绍

概念

面向对象编程是对数据进行抽象;函数式编程是对行为进行抽象,函数式编程只在乎数据以及对数据做了什么操作,面向对象里的类,对象,方法,数据类型都不关心。

为什么学习

  1. 代码简洁,开发速度快。
  2. 为了能够看懂框架、同事的代码。
  3. 使代码的可读性更高。
  4. 在大数据量下,对于集合的处理效率更高。

优点

  • 代码简洁,开发快速
  • 接近自然语言,易于理解
  • 易于"并发编程"

Lambda表达式

概念

Lambda表达式可以对某些匿名内部类的写法进行简化,它是JDK8中的一个语法糖。它是函数式编程思想的一个重要体现,让我们不用再关注业务逻辑里的对象,而只需关注我们要对数据进行什么操作。

核心原则

只要可以推导,都可以省略,即可以根据上下文推导出来的内容,都可以省略书写

语法格式

(参数列表) -> {代码}

示例一

在创建线程并启动时,我们可以使用匿名内部类的写法。

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("我要的只是你在我身边");
    }
}).start();

使用Lambda格式对以上代码进行修改,可以写出如下代码

new Thread(() -> System.out.println("我要的只是你在我身边")).start();

示例二

有如下方法定义,IntBinaryOperator是一个接口,内部有一个抽象方法applyAsInt

public static int addition(IntBinaryOperator opt) {
    int a = 82, b = 18;
    return opt.applyAsInt(a, b);
}

需求:调用以上代码,完成两个整数求和。

//使用匿名内部类调用方法。
int sum = addition(new IntBinaryOperator() {
    @Override
    public int applyAsInt(int left, int right) {
        return left + right;
    }
});
System.out.println(sum);
//使用Lambda表达式替换匿名内部类写法
int sum1 = addition((left, right) -> { return left + right; });
System.out.println(sum1);

示例三

调用以下方法,打印输出偶数。

public static void showOddNum(IntPredicate predicate) {
    int[] nums = {10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20};
    for (int num : nums)
        if (predicate.test(num))
           System.out.print(num + "  ");
}

public static void main(String[] args) {
    //匿名内部类调用
    showOddNum(new IntPredicate() {
        @Override
        public boolean test(int value) {
            return value%2 == 0;
        }
    });

    showOddNum(value -> value%2 == 0);
}

示例四

把字符串整数转成int型整数

public static int typeConvert(Function<String, Integer> function) {
    String s = "12345";
    return function.apply(s);
}

public static void main(String[] args) {
    //匿名内部类调用
    int i = typeConvert(new Function<String, Integer>() {
        @Override
        public Integer apply(String s) {
            //work
            return Integer.valueOf(s);
        }
    });
    System.out.println(i);

    int i1 = typeConvert(s -> Integer.valueOf(s));
    System.out.println(i1);
}

示例五

遍历数组

public static void traverseArr(IntConsumer consumer) {
    int[] nums = {10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20};
    for (int num : nums)
        consumer.accept(num);
}

public static void main(String[] args) {
    traverseArr(new IntConsumer() {
        @Override
        public void accept(int value) {
            System.out.print(value + " ");
        }
    });

    traverseArr(value -> System.out.print(value + " "));
}

省略原则

  1. 参数类型可以省略。
  2. 当方法体内只有一行代码时,return可以省略,分号可以省略,大括号也可以省略。
  3. 方法只有一个参数时,圆括号可以省略。

Stream流

概念

Stream 就好像一个高级的迭代器,但只能遍历一次,就好像一江春水向东流;在流的过程中,对流中的元素执行一些操作,比如“过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream流可以更方便我们对集合和数组的操作。

快速入门

需求:获取作家的集合,打印年龄小于18的作家的名字,并去重。

public static void main(String[] args) {
    //获取作家的集合,打印年龄小于18的作家的名字,并去重。
    List<Author> authors = getAuthors();
    authors.stream()//把集合转成流
            .distinct()//去重
            .filter(author -> author.getAge() < 18)//在流中获取年龄小于18的作者
            .forEach(author -> System.out.println(author.getName()));//遍历打印流中的数据
}

private static List<Author> getAuthors() {
    Author author1 = new Author(1L, "蒙多", 33, "一个从菜刀中明白哲理的祖安人", null);
    Author author2 = new Author(2L, "亚索", 15, "最美的不是下雨天", null);
    Author author3 = new Author(3L, "温", 14, "你知道我想要的只是在你身边", null);
    Author author4 = new Author(3L, "温", 14, "你知道我想要的只是在你身边", null);

    List<Book> books1 = new ArrayList<>();
    List<Book> books2 = new ArrayList<>();
    List<Book> books3 = new ArrayList<>();

    books1.add(new Book(1L, "刀的两侧是光明与黑暗", "哲理, 爱情", 88, "用一把刀划分了爱恨"));
    books1.add(new Book(2L, "一个人不能倒在同一把刀下", "个人成长, 爱情", 99, "讲述如何从失败中明悟哲理"));

    books2.add(new Book(3L, "到有风的地方去", "哲学", 85, "带你用思维去领略世界的尽头"));
    books2.add(new Book(3L, "到有风的地方去", "哲学", 85, "带你用思维去领略世界的尽头"));
    books2.add(new Book(4L, "吹或者不吹", "爱情,个人传记", 56, "一个哲学家的恋爱观注定很难把他所在的时代理解"));

    books3.add(new Book(5L, "你的剑就是我的剑", "爱情", 56, "无法想象一个武者能对他的伴侣如此宽容"));
    books3.add(new Book(6L, "风与剑", "个人传记", 100, "两个哲学家灵魂和肉体的碰撞会激起怎样的火花呢?"));
    books3.add(new Book(6L, "风与剑", "个人传记", 100, "两个哲学家灵魂和肉体的碰撞会激起怎样的火花呢?"));

    author1.setBooks(books1);
    author2.setBooks(books2);
    author3.setBooks(books3);
    author4.setBooks(books3);

    //把多个作者存储到一个集合里
    List<Author> authorList = new ArrayList<>(Arrays.asList(author1, author2, author3, author4));
    return authorList;
}

public class Author {
    private Long id;
    private String name;
    private Integer age;
    private String info;
    private List<Book> books;
}
public class Book {
    private Long id;
    private String name;
    private String category;
    private Integer score;
    private String info;
}

常用操作

创建流

集合获取流对象

集合引用名.stream()

public static void main(String[] args) {
    //List<String> list = new ArrayList<>();
    //Set<String> list = new Hashset<>();
    List<String> list = new LinkedList<>();
    list.add("a");
    list.add("b");
    list.add("c");
    list.add("c");
    list.add("a");
    //获取流对象
    list.stream()//获取流对象
            .distinct()//去重
            .forEach(s -> System.out.println(s));//终结打印内容
}
数组获取流对象

Arrays.stream(数组)或`Stream`.of(数组)来创建

public static void main(String[] args) {
    Integer[] nums = {10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20};
    Arrays.stream(nums)
            .filter(value -> value%2 == 0)
            .forEach(v -> System.out.println(v));

    Stream.of(nums)
            .filter(value -> value%2 == 0)
            .forEach(v -> System.out.println(v));
}
Map获取流对象

map.entrySet().stream()

public static void main(String[] args) {
    Map<String, Integer> map = new HashMap<>();
    map.put("a", 97);
    map.put("b", 98);
    map.put("c", 99);
    map.put("d", 100);
    map.put("e", 101);
    //获取map的流
    map.entrySet().stream()
                    .filter(entry -> entry.getValue() < 100)
                    .forEach(v -> System.out.println(v));
}

中间操作

filter: filter是个方法。方法参数接收一个接口的实现类,用户可以在接口中定义的方法内实现自己的逻辑来过了流中的数据

如以下示例过滤掉map集合中的value值大于100的数据

Map<String, Integer> map = new HashMap<>();
    map.put("a", 97);
    map.put("b", 98);
    map.put("c", 99);
    map.put("d", 100);
    map.put("e", 101);
    //获取map的流
    map.entrySet().stream()
                    .filter(entry -> entry.getValue() < 100)
                    .forEach(v -> System.out.println(v));

map:把一种流转换成另一种指定的流

public static void main(String[] args) {
    //获取作家的集合,打印年龄小于18的作家的名字,并去重。
    List<Author> authors = getAuthors();
    authors.stream()
            .map(author -> author.getInfo())
            .distinct()
            .forEach(value -> System.out.println(value));
}

flatMap:把一种流转换成另一种指定的流。它和map函数的区别是

authors.stream()
        .flatMap(author -> author.getBooks().stream())
        .distinct()
        .forEach(v -> System.out.println(v));

distinct: 去除重复元素,如果是自定义的类,那么该类需要重写equals和hashcode方法。否则会去重失败。

List<Author> authors = getAuthors();
authors.stream()
        .distinct()
        .forEach(v -> System.out.println(v));

sort : 给指定元素排序

List<Author> authors = getAuthors();
authors.stream()
        .sorted()//使用实现Comparable接口重写的compareTo方法进行自定义排序
        .forEach(v -> System.out.println(v.getName() + " " + v.getAge()));

List<Author> authors = getAuthors();
authors.stream()
        .sorted((o1, o2) -> o1.getName().compareTo(o2.getName()))//按照Comparetor排序
        .forEach(v -> System.out.println(v.getName() + " " + v.getAge()));

limit : 限制输出几条数据

authors.stream()
        .flatMap(author -> author.getBooks().stream())
        .limit(5)
        .forEach(v -> System.out.println(v));

skip : 跳过指定数量的数据条数,比如传入参数为3,那么就会忽略掉前三条数据。

List<Author> authors = getAuthors();
authors.stream()
        .flatMap(author -> author.getBooks().stream())
        .skip(5)
        .forEach(v -> System.out.println(v));

终结操作

forEach : 遍历打印

count : 流里的数据经过一系列函数处理后,最终想要获取一共有多少条数据,就可以使用该函数

List<Author> authors = getAuthors();
long count = authors.stream()
        .flatMap(author -> author.getBooks().stream())
        .count();
System.out.println(count);

max&min : 获取流中的最大最小值

List<Author> authors = getAuthors();
Optional<Integer> max = authors.stream()
        .map(author -> author.getAge())
        .max((o1, o2) -> o1 - o2);
System.out.println(max.get());

Optional<Integer> min = authors.stream()
        .map(author -> author.getAge())
        .min((param1, param2) -> param1 - param2);
System.out.println(min.get());

collect

把流转成集合

List<Author> authors = getAuthors();
List<Book> bookList = authors.stream()
        .flatMap(author -> author.getBooks().stream())
        .collect(Collectors.toList());
System.out.println(bookList);
//转Set
authors.stream()
        .map(author -> author.getAge())
        .collect(Collectors.toSet())
        .forEach(v -> System.out.println(v));
//转map
Map collect = authors.stream()
        .distinct()
        .collect(Collectors.toMap(author -> author.getName(), author -> author.getInfo()));
//collect转map需要传入两个Function,第一个是求出map的key,第二个是求出value
System.out.println(collect);

查找与匹配

anyMatch

查看流中是否有指定数据,有则返回true,否则返回false。

List<Author> authors = getAuthors();
boolean isMatch = authors.stream()
        .anyMatch(author -> author.getName().contains("多"));
System.out.println(isMatch);
allMatch

流里所有的数据都要包含指定的内容,如下示例,所有作家名字里都要包含温才会返回true。

List<Author> authors = getAuthors();
boolean isMatch = authors.stream()
        .allMatch(author -> author.getName().contains("温"));
System.out.println(isMatch);
findAny

返回流中任意一个元素

Optional<Author> any = authors.stream()
        .findAny();
System.out.println(any.get());
findFirst

返回流中第一个元素

Optional<Author> any = authors.stream()
        .findFirst();
System.out.println(any.get());

reduce归并

/**
 *     T result = identity;
 *      *     for (T element : this stream)
 *      *         result = accumulator.apply(result, element)
 *      *     return result;
 */

Integer[] nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
Integer result = Stream.of(nums)
        .reduce(0, ((sum, index) -> sum += index));
System.out.println(result);
//sum : 代表累加后的结果
//index : 表示数组下标

List<Author> authors = getAuthors();
Integer reduce = authors.stream()
        .map(author -> author.getAge())
        .reduce(0, ((sum1, i) -> sum1 += i));
System.out.println(reduce);

Optional

概述

我们在写代码的时候,经常会有出现空指针异常,这时候我们需要写if作非空判断。例如:

如果一个对象的属性过多,那么就要写大量的if,这时候代码就会变得很臃肿。

所以JDK8开始引入了Optional,就可以避免这种做非空判断的逻辑代码,使编写的代码更简洁。

使用

创建对象

Optinal原理:可以把我们具体数据封装到Optional对象内部。然后使用Optional中封装好的方法去操作封装进去的数据,就可以优雅的避免空指针异常

Optional常使用的方法是ofNullable,这是一个静态方法,无论传入的参数是否为null都不会出现问题。

Author author = getAuthor();
Optional<Author> optional = Optional.ofNullable(author);

这种方式初看觉得复杂,但是如果我们在getAuthor()方法内部就把对象封装为Optinal返回,使用时就会方便许多。

在实际开发中我们的数据几乎都是从数据库获取,Mybatis从3.5版本开始支持Optional。可以直接把dao层方法的返回值类型定义为Optional,Mybatis会自动把数据封装成Optional返回。封装的过程不需要我们写代码操作。

如果确定一个对象不是空的则可以使用Optional的静态方法of来把数据封装成Optional对象。

Author author = new Author();
Optional<Author> optional = Optional.of(author);

注意:使用of时传入的参数必须不能为null。

如果一个方法的返回值类型为Optional。但我们经过判断发型某次计算的结果却是null,这时候就需要把null封装成Optional对象。这时候可以使用Optional的静态方法empty来进行封装。

Optional.empty();

消费值

获取到一个Optional后肯定需要对数据进行使用。这时可以使用ifPresent。该方法会判断内部数据是否空。不空才会使用,这种方式使代码更安全。

public static void main(String[] args) {
    Optional<List<Author>> authors1 = Optional.ofNullable(getAuthors());
    authors1.ifPresent(authors2 -> System.out.println(authors2.get(1)));
}

获取值

get(): 不推荐,不安全

orElseGet() :在使用一个引用前,该引用可能返回的值会是null,那此时我们可以使用该方法给该引用一个默认值

如下代码,如果getAuthor()返回null,那么此时可以使用Lambda表达式里的:new Author(2L, "亚索", 15, "最美的不是下雨天", null)对象

getAuthor().orElseGet(() -> new Author(2L, "亚索", 15, "最美的不是下雨天", null));

orElseThrow()

try {
    getAuthor().orElseThrow((Supplier<Throwable>) () -> new RuntimeException("该对象为空"));
} catch (Throwable throwable) {
    throwable.printStackTrace();
}

过滤

filter()

和Stream流里一样的使用方式

判断

isPresent()

Optional<Author> optional = getAuthor();
optional.ifPresent(author -> System.out.println(author.getName()));
if(optional.isPresent()) {
    System.out.println(optional.get());
}

数据转换

map()

和Stream流里一样的使用方式

函数式接口

概述

Jdk8提供的新功能,接口中只有一个抽象方法,并且有注解@FunctionalInterface

常见函数式接口

Consumer、Predicate、Runnable

img

自定义函数式接口

@FunctionalInterface
public interface MyFunctionalInterface {
    public abstract int sum(int min, int max);
    default void sum1() {
        System.out.println("sum1()");
    }
}

public static void main(String[] args) {
    getSum((min, max) -> min + max);
}

private static void getSum(MyFunctionalInterface function) {
    int sum = function.sum(1, 100);
    System.out.println(sum);
}

方法引用

概述

如果在使用Lambda表达式的时候,方法体中只有一个方法(包括构造方法)调用时,可以使用方法引用,让Lambda表达式的写法看起来更加简洁。

基本语法格式:类名或对象名::方法名

引用静态方法

使用前提1:如果我们在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个类的静态方法,我们把重写的抽象方法中所有的参数都按照顺序传入了这个静态方法,这时候就可以引用类的静态方法。

使用前提2:重写方法时,方法中只有一行代码,并且这行代码是调用了第一个参数的成员方法,并且把要重写的抽象方法中剩余的所有的参数都按照顺序传入了这个方法中,这时候就可以使用方法引用。

格式:类名::方法名

引用对象的实例方法

使用前提:重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个对象的成员方法,我们把重写的抽象方法中所有的参数都按照顺序传入了这个成员方法中,这时候就可以引用对象的实例方法。

格式:对象名::方法名

构造方法引用

使用前提:重写方法时,只有一行代码,并且这行代码调用了某个类的构造方法,且我们把要重写的抽象方法中的所有参数都按照顺序传入了这个构造方法中,这时候我们就可以引用构造方法。

格式:类名::new

List<Author> authors = getAuthors();
authors.stream()
        .map(author -> author.getName())
        .map(String::new).count();
0

评论 (0)

取消