IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Java从入门到精通(一) -> 正文阅读

[Java知识库]Java从入门到精通(一)

Java

文章目录

Java 语言基础

Java 语言概述

image-20211020073312700

基础常识

软件:即一系列按照特定顺序组织的计算机数据和指令集合。分为:系统软件和应用软件

  • 系统软件 :Windows Linux macOS Unix Android iOS……
  • 应用软件:Word PowerPoint 画图板……

人机交互方式:图形化界面 vs 命令行方式

应用程序 = 算法 + 数据结构

常用 DOS 命令

dir:列出点钱目录下的文件以及文件夹
md:创建目录
rd:删除目录
cd:进入指定目录
cd..:退回到上一级目录
cd/:退回到根目录
del:删除文件
exit:退出 DOS 命令行

计算机语言的发展迭代史

  • 第一代:机器语言
  • 第二代:汇编语言
  • 第三代:高级语言
    • 面向过程:C,Pascal, Fortran
    • 面向对象:Java,JS,Python,Scala……

Java 语言版本迭代

  • 1991年 Green 项目,开发语言最初命名为 Oak (橡树)
  • 1994年,开发组意识到 Oak 非常适合于互联网
  • 1996年,发布 JDK 1.0,约8.3万个网页应用Java技术来制作
  • 1997年,发布 JDK 1.1,JavaOne 会议召开,创当时全球同类会议规模之最
  • 1998年,发布 JDK 1.2,同年发布企业平台J2EE
  • 1999年,Java 分成 J2SE、J2EE 和 J2ME,JSP/Servlet 技术诞生
  • 2004年,发布里程碑式版本:JDK 1.5,为突出此版本的重要性,更名为 JDK 5.0
  • 2005年,J2SE -> JavaSE,J2EE -> JavaEE,J2ME -> JavaME
  • 2009年,Oracle 公司收购 SUN,交易价格74亿美元
  • 2011年,发布 JDK 7.0
  • 2014年,发布 JDK 8.0,是继 JDK 5.0 以来变化最大的版本
  • 2017年,发布 JDK 9.0,最大限度实现模块化
  • 2018年3月,发布 JDK 10.0,版本号也称为18.3
  • 2018年9月,发布 JDK 11.0,版本号也称为18.9

Java语言的应用领域

  • Java Web 开发:后台开发
  • 大数据开发
  • Android 应用程序开发:客户端开发

Java 语言的特点

  • 面向对象

    • 两个要素:类、对象
    • 三个特征:封装、继承、多态
  • 健壮性

    • 去除了 C语言中的指针

    • 自动的垃圾回收 --> 仍然会出现内存溢出、内存泄漏

    • 跨平台性:write once, run anywhere:一次编译,到处运行

      功劳归功于:JVM

      image-20211020151648745

JDK、JRE、JVM 的关系

image-20211020151945857

JDK 的下载、安装和 path 环境变量的配置

为什么配置 path 环境变量?

path 环境变量:Windows 重装系统执行命令时所要搜寻的路径

为什么要配置 path:希望 Java 的开发工具在任何文件的文件路径下都可以执行成功

如何配置

https://blog.csdn.net/weixin_43344151/article/details/118917382

访问官网,下载对应的版本

https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html

在这里插入图片描述

安装

注意问题:安装软件的路径中不能包含中文、空格

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

配置环境变量

此电脑 --> 右击“属性” --> 点击“高级系统设置” --> 点击“环境变量”

在这里插入图片描述

双击系统变量的PATH
在这里插入图片描述

依次点击确定

打开cmd窗口
运行javajavacjava -version出现以下画面代表安装配置成功

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

开发体验 —— HelloWorld

image-20211020153000314

编写

创建一个 Java 源文件:HelloWorld.java

public class HelloWorld{
    public static void main(String[] args){
        System.out.println("Hello, World!");
    }
}
编译
javac HelloWorld.java
运行
java HelloWorld

常见问题解决

image-20211020153928916

image-20211020154008374

总结第一个程序

  1. Java程序编写、编译、运行的过程
    编写:将编写的 Java代码保存在以 .java 结尾的源文件中
    编译:使用 javac.exe 命令编译我们的 Java 源文件,格式:javac 源文件名.java
    运行:使用 java.exe 命令解释运行我们的字节码文件,格式:java 类名

  2. 在一个 Java 源文件中可以声明多个 class,但是只能最多有一个类声明为 public 的,而且要求声明为 public 的类的类名必须与源文件名相同

  3. 程序的入口是 main() 方法,格式是固定的

  4. 输出语句:

    System.out.println():先输出数据,然后换行
    System.out.print():只输出数据

  5. 每一行执行语句都以 ; 结束

  6. 编译的过程:编译以后,会生成一个或多个字节码文件,字节码文件的文件名跟 Java 源文件中的类名相同

注释:Comment

  • 分类:
    • 单行注释://
    • 单行注释:/* */
    • 文档注释:/** */
  • 作用:
    • 对所写的程序进行解释说明,增强可读性,方便自己,方便别人
    • 调试所写的代码
  • 特点:
    • 单行注释和多行注释,注释了的内容不参与编译,换句话说,编译以后生成的 .class 结尾的字节码文件不包含注释掉的内容
    • 文档注释所注释的内容可以被 JDK 提供的工具 javadoc 所解析,生成一套以网页文件形式体现的该程序的说明文档
    • 多行注释不可以嵌套使用

Java API 文档

API:application programming interface

习惯上:将语言提供的类库,都称为 API

API 文档:针对于提供的类库如何使用,给的一个说明书

良好的编程风格

  • 正确的注释和注释风格

    • 使用文档注释来注释整个类或整个方法。
    • 如果注释方法中的某一个步骤,使用单行或多行注释。
  • 正确的缩进和空白

    • 使用一次tab操作,实现缩进
    • 运算符两边习惯性各加一个空格。比如:2 + 4 * 5。
  • 块的风格

    • Java API 源代码选择了行尾风格

      image-20211020160400668

开发工具

  • 文本编辑工具
    • 记事本
    • UltraEdit
    • EditPlus
    • Notepad
    • TextPad
  • Java IDE
    • JBuilder
    • NetBeans
    • Eclipse
    • MyEclipse
    • IDEA

基本语法

关键字与标识符

关键字

定义:被 Java 语言赋予了特殊含义,用做专门用途的字符串(单词)

特点:关键字中所写字母都为小写

具体关键字:

image-20211028082910868

image-20211028082955423

保留字

定义:现有Java版本尚未使用,但以后版本可能会作为关键字使用。

具体哪些保留字:goto、const

注意:自己命名标识符时要避免使用这些保留字

标识符

定义:凡是自己可以起名字的地方都叫标识符

涉及到的结构:包名、类名、方法名、变量名、接口名、常量名

规则:(必须遵守,否则,编译不通过)

  1. 由26个英文字母大小写,0-9 ,_或$ 组成
  2. 数字不可以开头。
  3. 不可以使用关键字和保留字,但能包含关键字和保留字。
  4. Java 中严格区分大小写,长度无限制。
  5. 标识符不能包含空格。

规范:(可以不遵守,不影响编译和运行,但是要求大家遵守

  1. 包名:多单词组成时所有字母都小写:xxxyyyzzz
  2. 类名、接口名:多单词组成时,所有单词的首字母大写:XxxYyyZzz
  3. 变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个 单词首字母大写:xxxYyyZzz
  4. 常量名:所有字母都大写。多单词时每个单词用下划线连接:XXX_YYY_ZZZ

注意点:

  • 在起名字时,为了提高阅读性,要尽量有意义,“见名知意”。
代码整洁之道
  • 介绍

    • 软件中随处可见命名。我们给变量、函数、参数、类和包命名。我们给源代码及源代码所在目录命名。
    • 这么多命名要做,不妨做好它。下文列出了取个好名字的几条简单规则。
  • 名副其实,见名知意

    • 变量名太随意,haha、list1、ok、theList 这些都没啥意义
  • 避免误导

    • 包含List、import、java等类名、关键字或特殊字;
    • 字母o与数字0,字母l与数字1等
    • 提防使用不同之处较小的名称。比如:XYZControllerForEfficientHandlingOfStrings与XYZControllerForEfficientStorageOfStrings
  • 做有意义的区分

    • 反面教材,变量名:a1、a2、a3
    • 避免冗余,不要出现Variable、表字段中避免出现table、字符串避免出现nameString,直接name就行,知道是字符串类型
    • 再比如:定义了两个类:Customer类和CustomerObject类,如何区分?
    • 定义了三个方法:getActiveAccount()、getActiveAccounts()、getActiveAccountInfo(),如何区分?
  • 使用读得出来的名称

    • 不要使用自己拼凑出来的单词,比如:xsxm(学生姓名);genymdhms(生成日期,年、月、日、时、分、秒)
    • 所谓的驼峰命名法,尽量使用完整的单词
  • 使用可搜索的名称

    • 一些常量,最好不直接使用数字,而指定一个变量名,这个变量名可以便于搜索到.
    • 比如:找MAX_CLASSES_PER_STUDENT很容易,但想找数字7就麻烦了。
  • 避免使用编码

    • 匈牙利语标记法
      • 即变量名表明该变量数据类型的小写字母开始。例如,szCmdLine的前缀sz表示“以零结束的字符串”。
    • 成员前缀
      • 避免使用前缀,但是Android中一个比较好的喜欢用m表示私有等,个人感觉比较好
    • 接口和实现
      • 作者不喜欢把接口使用I来开头,实现也希望只是在后面添加Impl
  • 避免思维映射

    • 比如传统上惯用单字母名称做循环计数器。所以就不要给一些非计数器的变量命名为:i、j、k等
  • 类名

    • 类名与对象名应该是名词与名词短语。如Customer、WikiPage、Account和AddressParser。避免使用Data或Info这样的类名。

    • 不能使动词。比如:Manage、Process

  • 方法名

    • 方法名应当是动词或者动词短语。如postPayment、deletePage或save
  • 别扮可爱

    • 有的变量名叫haha、banana

    • 别用eatMyShorts()表示abort()

  • 每个概念对应一个词

    • 项目中同时出现controllers与managers,为什么不统一使用其中一种?

    • 对于那些会用到你代码的程序员,一以贯之的命名法简直就是天降福音。

  • 别用双关语
    有时可能使用add并不合适,比例insert、append。add表示完整的新添加的含义。

  • 使用解决方案领域名称

    • 看代码的都是程序员,所以尽量用那些计算机科学术语、算法名、模式名、数学术语,

    • 依据问题所涉领域来命名不算是聪明的做法。

  • 使用源自所涉问题领域的名称

    • 如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。

    • 至少,负责维护代码的程序员就能去请教领域专家了。

  • 添加有意义的语境

    • 可以把相关的变量放到一个类中,使用这个类来表明语境。
  • 不要添加没用的语境

    • 名字中带有项目的缩写,这样完全没有必要。比如有一个名为“加油站豪华版”(Gas Station Deluxe)的项目,
    • 在其中给每个类添加GSD前缀就不是什么好策略。
  • 最后的话

    • 取好名字最难的地方在于需要良好的描述技巧和共有文化背景

变量的使用

变量的分类
按数据类型分类

image-20211028121102168

详细说明:

  1. 整型:byte(1字节) short(2字节) int(4字节) long(8字节)
    1. byte 范围 -128~127
    2. 声明 long 型变量,必须以 “l” 或 ”L” 结尾
    3. 通常,定义整型变量时,使用 int 型
    4. 整型的常量默认类型是 int 型
  2. 浮点型:float (4字节) double(8字节)
    1. 浮点型表示带小数点的数值
    2. float 表示数值的范围比 long 还大
    3. 定义 float 类型变量时,变量要以 “f” 或 “F” 结尾
    4. 通常,定义浮点型变量时,使用 double 型
    5. 浮点型的常量,默认类型为 double
  3. 字符型:char (2字节)
    1. 定义 char 型变量,通常使用一对 ‘’ ,内部只能写一个字符
    2. 表示方式
      1. 声明一个字符
      2. 转义字符
      3. 直接使用 Unicode 值来表示字符型常量
  4. 布尔型:boolean
    1. 只能取两个值之一:true false
    2. 通常在条件判断、循环结构中使用
按声明的位置分类

image-20211029221953386

定义变量的格式

数据类型 变量名 = 变量值;

或者

数据类型 变量名;

变量名 = 变量值;

变量使用的注意点
  • 变量必须先声明,后使用

  • 变量都定义在其作用域内。在作用域内,它是有效的。换句话说,出了作用域就失效了

  • 同一个作用域中不可以说明两个重名的变量

基本数据类型变量间运算规则
涉及到的基本数据类型

除了 boolean 类型之外的其它7种

自动类型转换(只涉及7种基本数据类型)

当容量小的数据类型的变量与容量大的数据类型的变量做运算时,结果 自动提升为容量大的数据类型

byte 、short 、char –> int –> long –> float –> double

特别地:当 byte 、short 、char 三种类型的变量做运算时,结果为 int 类型

说明:此时的容量大小指的是,表示数的范围的大小,比如,float 容量要大于 long 的容量

强制类型转换(只涉及7种基本数据类型)

自动类型提升运算的逆过程

  • 需要使用强制符:()
  • 注意点:强制类型转换,可能导致精度损失
String 与 8种基本数据类型间的运算
  1. String 属于引用数据类型,翻译为:字符串
  2. 声明 String 类型变量时,使用一对 “”
  3. String 可以和8种基本数据类型做运算,且运算只能是连接运算:+
  4. 运算的结果仍然是 String 类型

避免

// 编译错误
String s = 123;
// 编译错误
int i = (int)"123";

进制

编程中涉及的进制及表示方式
  • 二进制(binary):0,1 ,满2进1.以0b或0B开头。
  • 十进制(decimal):0-9 ,满10进1。
  • 八进制(octal):0-7 ,满8进1. 以数字0开头表示。
  • **十六进制(hex):**0-9及A-F,满16进1. 以0x或0X开头表示。此处的A-F不区分大小写。 如:0x21AF +1= 0X21B0
二进制的使用说明
  • 计算机底层存储方式:所有数字在计算机底层都以二进制形式存在
  • 二进制数据的存储方式:所有的数值,不管正负,底层都以补码的形式存储
  • 原码、反码、补码的说明
    • 正数
      • 三码合一
    • 负数
      • 原码:直接将一个数值换成二进制,最高位是符号位
      • 反码:对原码按位取反,只是最高位(符号位)确定为1
      • 补码:其反码加1
进制间的转换

image-20211030012030600

运算符

算术运算符

代码

/*
    运算符之一:算术运算符
        + - + - * / % (前)++ ++(后) (前)-- --(后) +
*/
public class AriTest {
    public static void main(String[] args) {
        // 除号:/
        int num1 = 12;
        int num2 = 5;
        int result1 = num1 / num2;
        // 2
        System.out.println(result1);
        int result2 = num1 / num2 * num2;
        // 10
        System.out.println(result1);
        double result3 = num1 / num2;
        // 2.0
        System.out.println(result3);
        // 2.0
        double result4 = num1 / num2 + 0.0;
        // 2.4
        double result5 = num1 / (num2 + 0.0);
        // 2.4
        double result6 = (double) num1 + num2;
        System.out.println(result5);
        System.out.println(result6);
        // 取余:%
        // 结果数的符号与被模数相同
        // 开发中,经常使用 % 来判断能否被除尽的情况
        int m1 = 12;
        int n1 = 5;
        System.out.println("m1 % n1 = " + m1 % n1);
        int m2 = -12;
        int n2 = 5;
        System.out.println("m1 % n1 = " + m2 % n2);
        int m3 = 12;
        int n3 = -5;
        System.out.println("m1 % n1 = " + m3 % n3);
        int m4 = -12;
        int n4 = -5;
        System.out.println("m1 % n1 = " + m4 % n4);
        // (前)++:先自增1,后运算
        // ++(后):先运算,后自增1
        int a1 = 10;
        int b1 = ++a1;
        int a2 = 10;
        int b2 = a2++;
        System.out.println("a1 = " + a1 + ", b1 = " + b1);
        System.out.println("a2 = " + a2 + ", b2 = " + b2);
        int a3 = 10;
        a3++;
        int b3 = a3;
        // 注意点:自增1不会改变本身变量的数据类型
        short s1 = 10;
        s1++;
        System.out.println(s1);
        byte b = 127;
        b++;
        System.out.println("b = " + b);
        // (前)--:先自减1,后运算
        // --(后):先运算,后自减1
        int a4 = 10;
        int b4 = --a4;
        System.out.println("a4 = " + a4 + ", b4 = " + b4);
        int a5 = 10;
        int b5 = --a5;
        System.out.println("a5 = " + a5 + ", b5 = " + b5);
    }
}

特别说明

  1. (前)++:先自增,后运算

    (后)++:先运算,后自增**

  2. (前)–:先自减,后运算

    (后)–:先运算,后自减

  3. 连接符:+ 只能使用在 String 与其它数据类型变量之间使用。

赋值运算符

代码

/*
 * 运算符之二:赋值运算符
 * = += -= *= /= %=
 */
public class SetValueTest {
	public static void main(String[] args) {
		// =:赋值符号
		int i1 = 10;
		int j1 = 10;

		int i2, j2;
		// 连续赋值
		i2 = j2 = 10;
		int i3 = 10, j3 =10;
		int num1 = 10;
		num1 += 2; // num1 = num1 + 2;
		System.out.println(num1);
		int num2 = 10;
		num2 %= 5; // num2 = num2 % 5;
		System.out.println(num2);
		short s1 = 10;
		// 不会改变变量本身的数据类型
		s1 += 2;
		System.out.println(s1);
		// 开发中,如果希望变量实现+2的操作,有几种方法?(前提:int num = 10)
		// 方式1:num = num + 2;
		// 方式2:num += 2; (推荐)
		// 开发中,如果希望变量实现+1的操作,有几种方法?(前提:int num = 10)
		// 方式1:num = num + 1;
		// 方式2:num += 1;
		// 方式3:num++; (推荐)
		// 练习1
		int i = 1;
		i *= 0.1;
		// 0
		System.out.println(i);
		i++;
		// 1
		System.out.println(i);
		// 练习2
		int m = 2;
		int n = 3;
		n *= m++;
		// 3
		System.out.println("m = " + m);
		// 6
		System.out.println("n = " + n);
		// 练习3
		int n1 = 10;
		n1 += (n1++) + (++n1);
		// 32
		System.out.println( n1);
	}
}

特别说明

  1. 运算结果不会改变变量本身的数据类型
赋值运算符

代码

/*
 * 运算符之三:比较运算符
 * ==  != > < >= <= instanceof
 * 结论:
 * 1.比较运算符的结果是 boolean 类型
 * 2.区分 == 和 =
 */
public class CompareTest {
	public static void main(String[] args) {
		int i = 10;
		int j = 20;
		System.out.println(i == j); // false
		System.out.println(i = j); // 20

		boolean b1 = true;
		boolean b2 = false;
		System.out.println(b2 == b1); // false
		System.out.println(b2 = b1); // true
	}
}

特别说明

  1. 比较运算符的结果都是 boolean 类型

  2. > < >= <= :只能使用在数值类型的数据之间

  3. == != :不仅可以使用在数值类型数据之间,还可以使用在引用类型变量之间

    Account acct1 = new Account(1000);
    Account acct2 = new Account(2000);
    boolean b1 = acct1 == acct2; // 比较两个 Account 是否是同一账户
    boolean b2 = acct1 != acct2;
    
逻辑运算符

代码

/**
* Filename : LogicTest.java
* Author : keke
* Creation time : 下午8:06:50 2021年10月30日
* Description : 运算符之四:逻辑运算符
* 				&& & || | ! ^
* 说明:
* 1.逻辑运算符操作的都是 boolean 类型的变量
*/
public class LogicTest {
	public static void main(String[] args) {
		// 区分 & 与 &&
		// 相同点:
		// 1.& 与 && 的运算结果相同
		// 2.当符号左边为 true 时,二者都会执行符号右边的运算
		// 不同点:
		// 当符号左边为 false 时,& 会执行符号右边的运算,&& 不会执行符号右边的运算
		// 开发中,推荐使用 &&
		boolean b1 = true;
		b1 = false;
		int num1 = 10;
		if (b1 & (num1++ > 0)) {
			System.out.println("我现在在北京");
		}else {
			System.out.println("我现在在南京");
		}
		System.out.println("num1 = " + num1);
		boolean b2 = true;
		b2 = false;
		int num2 = 10;
		if (b2 && (num2++ > 0)) {
			System.out.println("我现在在北京");
		}else {
			System.out.println("我现在在南京");
		}
		System.out.println("num2 = " + num2);

		// 区分:| 与 ||
		// 相同点:
		// 1.| 与 || 的运算结果相同
		// 2.当符号左边为 false 时,二者都会执行符号右边的运算
		// 不同点:
		// 当符号左边为 true 时,| 会执行符号右边的运算,|| 不会执行符号右边的运算
		// 开发中,推荐使用 ||
		boolean b3 = false;
		b3 = true;
		int num3 = 10;
		if (b3 | (num3++ > 0)) {
			System.out.println("我现在在北京");
		}else {
			System.out.println("我现在在南京");
		}
		System.out.println("num3 = " + num3);
		boolean b4 = false;
		b4 = true;
		int num4 = 10;
		if (b4 || (num4++ > 0)) {
			System.out.println("我现在在北京");
		}else {
			System.out.println("我现在在南京");
		}
		System.out.println("num4 = " + num4);
	}
}

特别说明

  1. 逻辑运算符的操作的都是 boolean 类型的变量,运算结果是 boolean 类型
位运算符

代码

/**
* Filename : BitTest.java
* Author : keke
* Creation time : 下午8:42:10 2021年10月30日
* Description : 运算符之五:位运算符(了解)& | ~ ^ << >> >>>
* 结论:
* 1.位运算符操作的都是整型的数据
* 2.<<:在一定范围内,向左移1位,相当于乘以2
*   >>:在一定范围内,右移1位,相当于除以2
*
* 面试题:最高效的计算 2 * 8 ? 2 << 3 或 8 << 1
*/

public class BitTest {
	public static void main(String[] args) {
		int i = 21;
		i = -21;
		System.out.println("i << 2 = " + (i << 2));
		System.out.println("i << 3 = " + (i << 3));
		System.out.println("i << 26 = " + (i << 26));
		System.out.println("i << 27 = " + (i << 27));
		System.out.println("i >> 2 = " + (i >> 2));
		int m = 12;
		int n = 5;
		System.out.println("m & n = " + (m & n));
		System.out.println("m | n = " + (m | n));
		System.out.println("m ^ n = " + (m ^ n));
		// 练习:交换两个变量的值
		int num1 = 10;
		int num2 = 20;
		System.out.println("num1 = " + num1 + ", num2 =" + num2);
		// 方式一:定义临时变量 (推荐)
		int temp = num1;
		num1 = num2;
		num2 = temp;
		System.out.println("num1 = " + num1 + ", num2 =" + num2);
		// 方式二:
		// 好处:不用定义临时变量
		// 弊端:
		// 1、相加操作可能超出存储范围
		// 2.有局限性:只能适用于数值类型
		num1 = num1 + num2;
		num2 = num1 - num2;
		num1 = num1 - num2;
		System.out.println("num1 = " + num1 + ", num2 =" + num2);
		// 方式三:使用位运算符
		num1 = num1 ^ num2;
		num2 = num1 ^ num2;
		num1 = num1 ^ num2;
		System.out.println("num1 = " + num1 + ", num2 =" + num2);
	}
}

面试题:最高效的计算 2 * 8 ?

答案: 2 << 3 或 8 << 1

特别说明

1.位运算符操作的都是整型的数据

2.<<:在一定范围内,每向左移1位,相当于乘以2

>>:在一定范围内,每向右移1位,相当于除以2

三元运算符

代码

/**
* Filename : SanYuanTest.java
* Author : keke
* Creation time : 下午9:16:12 2021年10月30日
* Description : 运算符之六:三元运算符
* 1.结构:(条件表达式) ? 表达式1 : 表达式2
* 2.说明
* 	1.条件表达式的结果为 boolean 类型
* 	2.根据条件表达式真或假,决定执行表达式1,还是表达式2
*     如果表达式为 true,则执行表达式1
*     如果表达式为 false,则执行表达式2
*   3.表达式1和表达式2要求是一致的
*   4.三元运算符是可以嵌套的
* 3.凡是可以使用三元运算符的地方,都可以改写成 if-else
* 4.如果程序既可以用三元运算符,也可以用 if-else,那么优先选择三元运算符
*   原因:简洁、执行效率高
*/
public class SanYuanTest {
	public static void main(String[] args) {
		// 获取两个整数的较大值
		int m = 12;
		int n = 5;
		int max = m > n ? m : n;
		System.out.println(max);
		double num = m > n ? 2 : 1.0;
		// m > n ? 2 : "n大"; // 编译错误
		// 不建议
		String maxStr = m > n ? "m 大" : m == n ? "m 和 n 相等" : "n 大";
		System.out.println(maxStr);
		// 获取三个数的最大值
		int n1 = 12;
		int n2 = 30;
		int n3 = -43;
		max = n1 > n2 ? n1 > n3 ? n1 : n3 : n2;
		System.out.println(max);
		if (m > n) {
			System.out.println(m);
		}else {
			System.out.println(n);
		}
	}
}

特别说明

  1. 条件表达式的结果为 boolean 类型

  2. 根据条件表达式真或假,决定执行表达式1,还是表达式2

    如果表达式为 true,则执行表达式1

    如果表达式为 false,则执行表达式2

  3. 表达式1和表达式2要求是一致的

  4. 三元运算符是可以嵌套的

流程控制

分支结构
if-else

格式

  • 结构一

    if(条件表达式){
        执行表达式
    }
    
  • 结构二

    if(条件表达式){
        执行表达式1
    } else{
        执行表达式2
    }
    
  • 结构三

    if(条件表达式){
        执行表达式1
    }else if(条件表达式){
        执行表达式2
    }
    ...
    else{
     	执行表达式n
    }
    

说明:

  1. else 结构是可选的
  2. 针对于条件表达式:
    • 如果多个条件表达式之间是“互斥”关系(或没有交集的关系),哪个判断和执行语句声明在上面函数下面,无所谓
    • 如果多个条件表达式之间有交集的关系,需要根据实际情况,考虑清除应该将哪个结构声明在上面
    • 如果多个条件表达式之间有包含的关系。通常情况下,需要将范围小的声明在范围大的上面,否则,范围小的没机会执行
  3. if-else 结构是可以相互嵌套的
  4. 如果 if-else 结构中的执行语句只有一行时,那么这一对大括号可以省略,但是不建议省略
switch-case

格式

switch(表达式){
    case 常量1:
        执行语句1;
        // break;
    case 常量2:
        执行语句2;
        // break;
    ...
    default:
        执行语句n;
        // break;
}

说明

  1. 根据 switch 表达式中的值,依次匹配各个 case 中的常量,一旦匹配成功,则进入相应 case 结构中,调用其执行语句。当调用完执行语句以后,则仍然继续向下执行其它 case 结构中的执行语句,直到遇到 break 关键字或此 switch-case结构末尾结束为止

  2. break 关键字可以使用在 switch-case 结构中,表示一旦执行到此关键字,就跳出 switch-case 结构

  3. switch结构中的表达式,只能是如下的6种数据类型之一:

    byte、short、char、int、枚举类型(JDK5.0新增)、String(JDK7.0新增)

  4. case 之后只能声明常量,不能声明范围

  5. break 关键字是可选的

  6. default 相当于 if-else 结构中的 else, default 结构是可选的,而且位置是灵活的

循环结构
循环结构的四要素
  1. 初始化条件
  2. 循环条件 --> 是 boolean 类型
  3. 循环体
  4. 迭代条件

说明:通常情况下,循环结束都是因为循环条件返回 false 了

for

结构

for(1;2;4){
    3
}

执行过程:1 --> 2 --> 3 --> 4 --> 2 --> 3 --> 4 --> … --> 2

while

结构

1
while(2){
    3;
    4;
}

执行过程:1 --> 2 --> 3 --> 4 --> 2 --> 3 --> 4 --> … --> 2

说明

  • 写 while 循环千万小心不要丢了迭代条件,一旦丢了,就可能导致死循环

for 和 while 循环总结

  1. 开发中,基本上都会从 for、while 中进行选择,实现循环结构

  2. for 循环和 while 循环是可以相互装换的

    区别:for 循环和 while 循环的初始化条件部分的作用范围不同

  3. 避免出现死循环

do-while

结构

1
do{
    3
    4;
}while(2);

执行过程:1 -->3 --> 4 --> 2 --> 3 --> 4 --> 2 --> … --> 2

说明

  1. do-while 循环至少会执行一次循环体
  2. 开发中,使用 for 和 while 更多一些,较少使用 do-while
“无限循环”结构
for(;;){

}

while(true){

}

总结:如何结束一个循环结构?

  • 当循环条件是 false 时
  • 在循环体中,执行 break
嵌套循环
  1. 嵌套循环:将一个循环结构 A 声明在另一个循环结构 B 的循环体中,就构成了嵌套循环

    外层循环:循环结构 B

    内层循环:循环结构 A

2.说明

  • 内层循环结构遍历一遍,只相当于外层循环循环体执行了一次
  • 假设外层循环需要执行 m 次,内层循环需要执行 n 次,此时内层循环的循环体一个执行了 m * n 次
  • 外层循环控制行数,内层循环控制列数

典型练习

package day05;

/**
* Filename : ForForTest.java
* Author : keke
* Creation time : 下午1:47:24 2021年11月1日
* Description :
* 嵌套循环的使用
* 1.嵌套循环:将一个循环结构 A 声明在另一个循环结构 B 的循环体中,就构成了嵌套循环
* 2.外层循环:循环结构 B
*   内层循环:循环结构 A
* 3.说明
*   1.内层循环结构遍历一遍,只相当于外层循环循环体执行了一次
*   2.假设外层循环需要执行 m 次,内层循环需要执行 n 次,此时内层循环的循环体一个执行了 m * n 次
*   3.外层循环控制行数,内层循环控制列数
*/
public class ForForTest {
	public static void main(String[] args) {
		for(int i = 1; i <= 4; i++) {
			for(int j = 1; j <= 6; j++) {
				System.out.print('*');
			}
			System.out.println();
		}
		for(int i = 0; i < 5; i++) {
			for(int j = 0; j <= i; j++) {
				System.out.print('*');
			}
			System.out.println();
		}
		for(int i = 0; i < 5; i++) {
			for(int j = 1; j <= 5 - i; j++) {
				System.out.print('*');
			}
			System.out.println();
		}
		System.out.println("==============================================");
		for(int i = 0; i < 5; i++) {
			for(int j = 0; j <= i; j++) {
				System.out.print('*');
			}
			System.out.println();
		}
		for(int i = 0; i < 5; i++) {
			for(int j = 2; j <= 5 - i; j++) {
				System.out.print('*');
			}
			System.out.println();
		}
	}
}

package day05;

/**
* Filename : NineNineTable.java
* Author : keke
* Creation time : 下午2:31:29 2021年11月1日
* Description :
* 九九乘法表
*/
public class NineNineTable {
	public static void main(String[] args) {
		for (int i = 1; i < 10; i++) {
			for(int j = 1; j <= i; j++) {
				System.out.print(j + " * " + i + " = " + i * j + "\t");
			}
			System.out.println();
		}
	}
}

package day05;

/**
* Filename : PrimeNumberTest2.java
* Author : keke
* Creation time : 下午2:41:08 2021年11月1日
* Description :
* 100以内所有的质数
* 质数:只能被1和它本身整除的自然数,又叫素数 --> 从2开始,到这个数-1结束为止,都不能被这个数本身整除
* 最小的质数:2
*/
public class PrimeNumberTest2 {
	public static void main(String[] args) {
		// 获取当前时间的毫秒数
		long start = System.currentTimeMillis();
		label:
		for(int i = 2; i <= 1000; i++) {
			for(int j = 2; j <= Math.sqrt(i); j++) {
				// i 被 j 除尽
				if (i % j == 0) {
					continue label;
				}
			}
			// 能执行到此步骤的,都是质数
			System.out.print(i + " ");
		}
		System.out.println();
		long end = System.currentTimeMillis();
		System.out.println(end - start);
	}
}

补充:衡量一个功能代码的优劣:

  1. 正确性
  2. 可读性
  3. 健壮性
  4. 高效率与低存储:时间复杂度、空间复杂度(衡量算法的好坏)
break 和 continue 关键字的使用
关键字使用范围循环中使用的作用相同点
breakswitch-case
循环结构结束当前循环关键字后面不能声明执行语句
continue循环结构结束当次循环关键字后面不能声明执行语句

带标签的 break 和 continue 关键字的使用

Scanner 类的使用
package day04;
import java.util.Scanner;
/**
* Filename : ScannerTest.java
* Author : keke
* Creation time : 下午3:23:13 2021年10月31日
* Description :
* 如何从键盘获取不同类型的变量:需要使用 Scanner 类
* 具体实现步骤:
* 1.导包:import java.util.Scanner;
* 2.Scanner 实例化:Scanner scanner = new Scanner(System.in);
* 3.调用 Scanner 类的相关方法(next() 和 nextXxx()),来获取指定类型的变量
* 注意:
* 需要根据相应的方法,来输入指定类型的值,如果深入到数据类型与要求的类型不匹配,则报异常 InputMismatchException
* 导致程序终止
*/
public class ScannerTest {
	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);
		System.out.print("请输入你的姓名:");
		String name = scanner.next();
		System.out.println(name);
		System.out.print("请输入你的年龄:");
		int age = scanner.nextInt();
		System.out.println(age);
		System.out.print("请输入你的体重:");
		double weight = scanner.nextDouble();
		System.out.println(weight);
		System.out.print("是否单身:");
		boolean isLove = scanner.nextBoolean();
		System.out.println(isLove);
		// 对于 char 型的获取,Scanner 没有提供相关的方法。只能获取一个字符串
		System.out.print("请输入你的性别:");
		// 获取索引为0位置上的字符
		char genderChar = scanner.next().charAt(0);
		System.out.println(genderChar);
	}
}

数组

数组的概述

  1. 数组:数组(Array),是多个相同类型数据按一定顺序排列的集合,并使用一个名字命名,并通过编号的方式对这些数据进行统一管理。
  2. 数组相关概念
  • 数组名
  • 元素
  • 角标、下标、索引
  • 数组的长度:元素的个数
  1. 数组的特点:

    1. 数组是有序排列的
    2. 数组属于引用数据类型的变量。数组的元素,既可以是基本数据类型,也可以是引用数据类型
    3. 创建数组对象会在内存中开辟一整块连续的空间
    4. 数组的长度一旦确定,就不能修改
  2. 数组的分类

    1. 按照维数:一维数组、二维数组、…
    2. 按照数组元素的类型:基本数据类型元素的数组、引用数据类型元素的数组

数据结构

  1. 数据与数据之间的逻辑关系:集合、一对一、一对多、多对多
  2. 数据的存储结构
    1. 线性表:顺序表(比如:数组)、链表、栈、队列
    2. 树形结构:二叉树
    3. 图形结构

算法

  1. 排序算法
  2. 搜索算法

一维数组

一维数组的声明和初始化
// 1.一维数组的声明和初始化
int[] ids;
// 1.1 静态初始化:数组的初始化和数组元素的赋值操作同时进行
ids = new int[]{1001, 1002, 1003, 1004};
int dids = {1001, 1002, 1003, 1004}; // 类型推断
// 1.2 动态初始化:数组的初始化和数组元素的赋值操作分开进行
String[] names = new String[5];
// 错误的写法
// int[] arr1 = new int[];
// int[5] arr1 = new int[];
// int[] arr1 = new int[3]{1, 2, 3};
一维数组的引用

通过角标的方式调用

// 数组的角标(或索引)从0开始,到数组的长度-1结束
names[0] = "王铭";
names[1] = "王赫";
names[2] = "张学良";
names[3] = "张居龙";
names[4] = "王宏志";
数组的属性

length

System.out.println(names.length);
System.out.println(ids.length);

注意

  • 数组一旦初始化完成,其长度就确定了
  • 数组长度一旦确定,就不可以修改
遍历一维数组
for(int i = 0; i < names.length; i++){
    System.out.print(names[i] + " ");
}
一维数组元素的默认初始化值
  • 数组元素是整型:0
  • 数组元素是浮点型:0.0
  • 数组元素是字符型:0 ‘\u0000’
  • 数组元素是布尔型:false
  • 数组元素是引用类型: null
数组的内存解析

image-20211102144825330

二维数组

如何理解二维数组

数组属于引用数据类型,数组的元素也可以是引用数据类型,一个一维数组的元素如果还是一个一维数组类型的,则此数组称为二维数组

二维数组的声明和初始化
// 静态初始化
int[][] arr1 = new int[][] {{1, 2, 3}, {4, 5}, {6, 7, 8}};
// 动态初始化
String[][] arr2 = new String[3][2];
String[][] arr3 = new String[3][];
// 错误的情况
// String[][] arr4 = new String[][3];
// 正确
int arr4[][] = new int[3][];
int[] arr5[] = new int[3][];
// 类型推断
int[] arr6[] = {{1, 2, 3}, {4, 5}, {6, 7, 8}};
二维数组的调用
System.out.println(arr1[0][1]);
System.out.println(arr2[1][1]);
arr3[1] =  new String[4];
System.out.println(arr3[1][0]);
二维数组的属性
System.out.println(arr4.length);
System.out.println(arr4[1].length);
遍历二维数组
for (int i = 0; i < arr6.length; i++) {
    for (int j = 0; j < arr6[i].length; j++) {
        System.out.print(arr6[i][j] + " ");
    }
    System.out.println();
}
数组元素的默认初始化值

规定:二维数组分为外层数组的元素,内层数组的元素

  • 针对于初始化方式一:比如:int[][] arr = new int[4][3];
    • 外层元素的初始化值为:地址值
      
    • 内层元素的初始化值为:与一维数组初始化情况相同
      
  • 针对于初始化方式二:比如:int[][] arr = new int[4][];
    • 外层元素的初始化值为:null
      
    • 内层元素的初始化值为:不能定义,否则报错
      
数组的内存解析

image-20211102145834778

数组的常见算法

数组的创建与元素赋值

杨辉三角(二维数组)、回形数(二维数组)、6个数,1-30之间随机生成且不重复

针对于数值型的数组

最大值、最小值、总和、平均数等

数组的赋值与复制
int[] array1, array2;
array1 = new int[]{1, 2, 3, 4};
赋值
array1 = array2;

理解:将 array1 保存的数组的地址值赋给了 array2,使得 array1 和 array2 共同指向堆空间的同一个数组实体

image-20211103105240926

复制
array2 = new int[array1.length];
for (int i = 0; i < array2.length; i++) {
    array2[i] = array1[i];
}

理解:通过 new 的方式,给 array2 在堆空间中新开辟了数组的空间,将 array1 数组中的元素值一个一个的赋值到 array2 数组中

image-20211103105528628

数组元素的反转
// 方式一:
for (int i = 0; i < arr1.length / 2; i++) {
    String temp = arr1[i];
    arr1[i] = arr1[arr1.length - i - 1];
    arr1[arr1.length - i - 1] = temp;
}
// 方式二:
for(int i = 0, j = arr1.length - 1; i < j; i++, j--) {
    String temp = arr1[i];
    arr1[i] = arr1[j];
    arr1[j] = temp;
}
数组中指定元素的查找
线性查找

实现思路:通过遍历的方式,一个一个的数据进行比较、查找

适用性:具有普遍适用性

二分法查找

实现思路:每次比较中间值,折半的方式检索

适用性:(前提 :数组必须有序)

/**
* 折半查找法
* @param arr 有序的数组
* @param dest 要找的数字
*/
public void binarySearch(int[] arr, int dest) {
    // 初始首索引
    int head = 0;
    // 初始末索引
    int end = arr.length  - 1;
    boolean isFlag1 = true;
    while (head <= end) {
        int middle = (head + end) / 2;
        if (dest == arr[middle]) {
            System.out.println("找到了指定的元素,位置为:" + middle);
            isFlag1 = false;
            break;
        }else if (arr[middle] > dest) {
            end = middle - 1;
        }else {
            head = middle + 1;
        }
    }
    if (isFlag1) {
        System.err.println("很遗憾,没有找到");
    }
}
数组的排序算法
  • 选择排序
    • 直接选择排序
    • 堆排序
  • 交换排序
    • 冒泡排序
    • 快速排序
  • 插入排序
    • 直接插入排序
    • 折半插入排序
    • 希尔排序
  • 归并排序
  • 桶式排序
  • 基数排序

理解

  • 衡量排序算法的优劣

    • 时间复杂度
    • 空间复杂度
    • 稳定性
  • 排序的分类

    • 内部排序
    • 外部排序(需要借助于磁盘)
  • 不同排序算法的时间复杂度

    排序方法时间复杂度(平均)时间复杂度(最坏)时间复杂度(最好)空间复杂度稳定性
    插入排序O(n2)O(n2)O(n)O(1)稳定
    希尔排序O(n1.3)O(n2)O(n)O(1)不稳定
    选择排序O(n2)O(n2)O(n2)O(1)不稳定
    堆排序O(nlog2n)O(nlog2n)O(nlog2n)O(1)不稳定
    冒泡排序O(n2)O(n2)O(n)O(1)稳定
    快速排序O(nlog2n)O(n2)O(nlog2n)O(nlog2n)不稳定
    归并排序O(nlog2n)O(nlog2n)O(nlog2n)O(n)稳定
    计数排序O(n+k)O(n+k)O(n+k)O(n+k)稳定
    桶式排序O(n+k)O(n2)O(n)O(n+k)稳定
    基数排序O(n*k)O(n*k)O(n*k)O(n+k)稳定
  • 手写冒泡排序

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

Arrays 工具类的使用

理解
  • 定义在 java.util 包下
  • Arrays:提供了很多操作数组的方法
使用
package com.atguigu.java;

import java.util.Arrays;

/**
* Filename : ArraysTest.java
* Author : keke
* Creation time : 下午9:52:42 2021年11月2日
* Description :
* java.util.Arrays:操作数组的工具类,里面定义了很多操作数组的方法
*/
public class ArraysTest {
	public static void main(String[] args) {
		// 1.boolean equals(int[] a,int[] b):判断两个数组是否相等。
		int[] arr1 = {1, 2, 3, 4};
		int[] arr2 = {1, 3, 2, 4};
		System.out.println(Arrays.equals(arr1, arr2));
		// 2.String toString(int[] a):输出数组信息。
		System.out.println(Arrays.toString(arr1));
		// 3.void fill(int[] a,int val):将指定值填充到数组之中。
		Arrays.fill(arr1, 10);
		System.out.println(Arrays.toString(arr1));
		// 4.void sort(int[] a):对数组进行排序。
		Arrays.sort(arr2);
		System.out.println(Arrays.toString(arr2));
		// 5.int binarySearch(int[] a,int key):对排序后的数组进行二分法检索指定的值。
		int[] arr3 = {-98, -34, 2, 34, 54, 66, 79, 105, 210, 333};
		System.out.println(Arrays.binarySearch(arr3, 211));
	}
}

数组的常见异常

数组角标越界异常:ArrayIndexOutOfBoundsException
int[] arr = {1, 2, 3, 4, 5};
for (int i = 0; i <= arr.length; i++) {
    System.out.println(arr[i]);
}
System.out.println(arr[-2]);
空指针异常:NullPointerException
// 情况一:
int[] arr1 = {1, 2, 3};
arr1 = null;
System.out.println(arr1[1]);
// 情况2:
int[][] arr2 = new int[4][];
System.out.println(arr2[0][0]);
// 情况3:
String[] arr3 = new String[] {null, "BB", "CC"};
System.out.println(arr3[0].toString());

小知识:一旦程序出现异常,未处理时,就终止执行

面向对象

类与对象

面向对象学习的三条主线
  1. Java 类及类的成员:属性、方法、构造器、代码块、内部类
  2. 面向对象的三大特征:封装、继承、多态、(抽象)
  3. 其它关键字:this、super、static、final、abstract、interface、package、import 等

“大处着眼,小处着手”

面向过程与面向对象
  • 面向过程,强调的是功能行为,以函数为最小单位,考虑怎么做。
  • 面向对象,强调具备了功能的对象,以类/对象为最小单位,考虑谁来做。
完成一个项目(或功能)的思路
  • 根据问题需要,选择问题所针对的现实世界中的实体
  • 从实体中寻找解决问题相关的属性和功能,这些属性和功能就形成了概念世界中的类
  • 把抽象的实体用计算机语言进行描述,形成计算机世界中类的定义。即借助某种程序 语言,把类构造成计算机能够识别和处理的数据结构。
  • 类实例化成计算机世界中的对象。对象是计算机世界中解决问题的最终工具。
面向对象中两个重要的概念
  • 类:对一类事物的描述,是抽象的、概念上的定义
  • 对象:是实际存在的该类事物的每个个体,因而也称为实例(instance)。
    • 面向对象程序设计的重点是类的设计
    • 设计类,就是设计类的成员

二者的关系:对象,是由类 new 出来的,派生出来的

面向对象思想落地实现的规则一
  1. 创建类,设计类的成员
  2. 创建类的对象
  3. 通过“对象.属性”或“对象.方法”,调用对象的结构

补充:几个概念的使用说明

  • 属性 = 成员变量 = field = 域 = 字段
  • 行为 = 成员方法 = method = 方法 = 函数
  • 创建类的对象 = 类的实例化 = 实例化类
对象的创建与对象的内存解析

典型代码

Person p1 = new Person();
Person p2 = new Person();
Person p3 = p1; // 没有新创建一个对象,共用一个堆空间的对象实体

说明:

  • 如果创建了一个类的多个对象,则每个对象都拥有一套类的属性(非 static 的)
  • 意味着:如果修改一个对象的属性 a,则不影响另外一个属性 a 的值

内存解析

image-20211104104047378

image-20211105113814418

匿名对象

概念:创建的对象,没有显示地赋给一个变量名,即为匿名对象

特点:匿名对象只能调用一次

举例

new Phone().sendEmail();
new Phone().playGame();
new Phone().price = 1999;
new Phone().showPrice();

应用场景

PhoneMall mall = new PhoneMall();
// 匿名对象的使用
mall.show(new Phone());
class PhoneMall{
    public void show(Phone phone) {
        phone.sendEmail();
        phone.playGame();
    }
}
理解“万事万物皆对象”
  1. 在 Java 语言范畴中,都将功能、结构等封装到类中,通过类的实例化,来调用类的具体功能结构
    • Scanner,String 等
      
    • 文件:File
      
    • 网络资源:URL
      
  2. 涉及到 Java 语言与前端 Html、后端数据库交互时,前后端的结构在 Java 层面交互时,都体现为类、对象
JVM 内存结构

编译完源程序以后,生成一个或多个字节码文件。使用 JVM 中的类的加载器和解释器对生成的字节码文件进行解释运行,意味着,需要将字节码文件对应的类加载到内存中,涉及到内存解析

image-20211104104803420

  • 虚拟机栈,即为平时提到的栈结构,将局部变量存储在栈结构中

  • 堆,将 new 出来的结构加载在堆空间中,补充:对象是属性(非 static 的)加载在堆空间中

  • 方法区:类的加载信息、常量池、静态域

类的结构之一:属性

类的设计中,两个重要的结构之一:属性

对比 属性 VS 局部变量

  1. 相同点

    1. 定义变量的格式:数据类型 变量名 = 变量值;
    2. 先声明,后使用
    3. 变量都有其对应的作用域
  2. 不同点:

    1. 在类中声明的位置的不同

      1. 属性:直接定义在类的一对{}内
      2. 局部变量:声明在方法内、方法形参、代码块内、构造器形参、构造器内部的变量
    2. 关于权限修饰符的不同

      1. 属性:可以在声明属性时,指明其权限,使用权限修饰符

        常用的权限修饰符:private、public、缺省、protected --> 封装性

        目前,声明属性时,都使用缺省就可以了

      2. 局部变量:不可以使用权限修饰符的

    3. 默认初始化值的情况

      1. 属性:类的属性根据其类型,都有默认初始化值
        1. 整型(byte、short、int、long):0
        2. 浮点型(float、double):0.0
        3. 字符型(char):0(或 ‘\u0000’)
        4. 布尔型(boolean):false
        5. 引用数据类型(类、数组、接口):null
      2. 局部变量:没有默认初始化值,意味着:在调用局部变量之前,一定要显示赋值,特别地:线程在调用时,赋值即可
    4. 在内存中加载的位置

      1. 属性:加载到堆空间(非 static)
      2. 局部变量:加载到栈空间

补充:回顾变量的分类

  • 方式一:按照数据类型

    image-20211028121102168

  • 方式二:按照在类中声明的位置

    image-20211029221953386

类的结构之二:方法

类的设计中,两个重要的结构之二:方法

  1. 方法的声明:

    权限修饰符 返回值类型 方法名(形参列表){
        方法体
    }
    

    注意:static、final、abstract 来修饰的方法,后面再说

  2. 说明

    1. 权限修饰符:默认方法的权限修饰符先都使用 public

      Java 规定的4种权限修饰符:private、protected、缺省、public --> 封装性再细说

    2. 返回值类型:有返回值 vs 没有返回值

      1. 如果方法有返回值,则必须在方法声明时,指定返回值的类型,同时,方法中,需要使用 return 关键字来返回指定类型的变量或常量:“return 数据;“
      2. 如果方法没有返回值,则方法声明时,使用 void 来表示,通常,没有返回值的方法中,就不能使用 return。但是,如果使用的话,只能 “return;” 表示结束此方法的意思
      3. 定义方法该不该有返回值?
        1. 题目要求
        2. 凭经验:具体问题具体分析
    3. 方法名:属于标识符,遵循标识符的命名规则和规范,“见名知义”

    4. 形参列表:方法可以声明零个,一个,或多个形参

      1. 格式:数据类型1 形参1, 数据类型2 形参2,…
      2. 定义方法时,该不该定义形参?
        1. 题目要求
        2. 凭经验:具体问题具体分析
    5. 方法体:方法功能的体现

  3. 方法的使用中,可以调用当前类的属性或方法

    1. 特殊的:方法 A 中又调用了方法 A 递归方法
    2. 方法中,不可以定义方法
return 关键字的使用
  1. 使用范围:使用在方法体中
  2. 作用
    1. 结束方法
    2. 针对于有返回值类型的方法,使用 “return 数据;” 方法返回所要的数据
  3. 注意点:return 关键字后面不可以声明执行语句
方法重载
概念

定义:在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数 类型不同即可。

总结:“两同一不同”

  • 同一个类,相同方法名
    
  • 参数列表不同:参数个数不同,参数类型不同
    
举例

Arrays 类中重载的 sort() binarySearch(),PrintStream 中的 println()

// 以下4个方法能构成重载
public void getSum(int i, int j) {
    System.out.println(1);
}

public void getSum(double d1, double d2) {
    System.out.println(2);
}

public void getSum(String s, int i) {
    System.out.println(3);
}

public void getSum(int i, String s) {
    System.out.println(4);
}

// 以下3个方法不能构成重载
public int getSum(int i, int j){
    return 0;
}

public void getSum(int m, int n){

}

private void getSum(int i, int j){

}
判断是否构成方法的重载

严格按照定义判断:两同一不同

跟方法的权限修饰符、返回值类型、形参变量名、方法体都没关系

如何确定类中某一个方法的调用

方法名 --> 参数列表

面试题:方法的重载与重写的区别?

throws / throw
String / StringBuilder / StringBuffer
Collection / Collections
final / finally / finalize
sleep() / wait()
接口 / 抽象类
...
可变个数形参的方法
使用说明
  1. jdk5.0 新增的内容
  2. 具体使用
    1. 可变个数形参格式:类型… 变量名
    2. 当调用可变个数形参时,传入的参数可以是0个,1个或多个
    3. 可变个数形参的方法与本类中方法名相同,形参不同的方法之间构成重载
    4. 可变个数形参的方法与本类中方法名相同,形参类型相同的数组之间不构成重载,换句话说,两者不能共存
    5. 可变个数形参在方法的形参中,必须声明在末尾
    6. 可变个数形参在方法的形参中,最多只能声明一个可变形参
举例说明
public class MethodArgsTest {
	public void show(int i) {

	}

	public void show(String s) {
		System.out.println("MethodArgsTest.show(String s)");
	}

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

    // 不能与上一个方法同时存在
	/*public void show(String[] strs) {
		System.out.println("MethodArgsTest.show(String[] strs)");
	}*/


	public void show(int i, String... strs) {

	}

}

调用时

public static void main(String[] args) {
    MethodArgsTest test = new MethodArgsTest();
    test.show(1);
    test.show("Hello");
    test.show("Hello", "World");
    test.show();
    test.show(new String[] {"Hello", "World", "!"});
}

Java 的值传递机制
针对于方法内变量的赋值举例
public class ValueTransferTest {
	public static void main(String[] args) {
		int m = 10;
		int n = m;

		System.out.println("m = " + m + ", n = " + n);
		n = 20;
		System.out.println("m = " + m + ", n = " + n);

		Order o1 = new Order();
		o1.orderId = 1001;

		Order o2 = o1;
		System.out.println("o1.orderId = " + o1.orderId + ", o2.orderId = " + o2.orderId);
		// 赋值以后,o1 和 o2 的地址值相同,导致都指向堆空间的一个对象实体
		o2.orderId = 1002;
		System.out.println("o1.orderId = " + o1.orderId + ", o2.orderId = " + o2.orderId);

	}
}

class Order{

	int orderId;
}

规则

  • 如果变量的基本数据类型,此时赋值的是变量所保存的数据值
  • 如果变量的引用数据类型,此时赋值的是变量所保存的数据的地址值
针对于方法的参数的概念
  • 形参:方法定义时,声明的小括号内的参数
  • 实参:方法调用时 ,实际传递给形参的数据
Java 中参数传递机制
  • 规则
    • 如果参数是基本数据类型,此时实参赋给形参的是实参真实存储的数据值
    • 如果参数是引用数据类型,此时实参赋给形参的是实参存储数据的地址值
  • 推广
    • 如果变量的基本数据类型,此时赋值的是变量所保存的数据值
    • 如果变量的引用数据类型,此时赋值的是变量所保存的数据的地址值
      
典型例题和内存解析

image-20211106113530572

image-20211106113612317

递归方法
定义

递归方法:一个方法体内调用它自身

理解
  • 方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制。
  • ?递归一定要向已知方向递归,否则这种递归就变成了无穷递归,类似于死循环。
举例
public class RecursionTest {

	// 例1:计算1-100之间所有自然数的和
	public int getSum(int n) {
		if (n == 1) {
			return 1;
		}
		return n + getSum(n - 1);
	}

	// 例2:计算1-100之间所有自然数的乘积
	public int getSum1(int n) {
		if (n == 1) {
			return 1;
		}
		return n * getSum1(n - 1);
	}

	/**
	 * 例3:已知有一个数列:f(0) = 1, f(1) = 4, f(n+2) = 2 * f(n+1) + f(n),
	 * 其中 n 是大于0的整数,求 f(10) 的值。
	 */
	public int f(int n) {
		if (n == 0) {
			return 1;
		}else if (n == 1) {
			return 4;
		}
		return 2 * f(n - 1) + f(n - 2);
	}

	// 例4:斐波那契数列
	public int f1(int n) {
		if (n == 1 || n == 2) {
			return 1;
		}
		return f(n - 1) + f(n - 2);
	}
}

面向对象的特征之一:封装与隐藏

为什么引入
  • 程序设计追求“高内聚,低耦合”。
    • 高内聚 :类的内部数据操作细节自己完成,不允许外部干涉;
    • 低耦合 :仅对外暴露少量的方法用于使用。
  • 隐藏对象内部的复杂性,只对外公开简单的接口。便于外界调用,从而提 高系统的可扩展性、可维护性。通俗的说,把该隐藏的隐藏起来,该暴露 的暴露出来。这就是封装性的设计思想。
问题引入

当创建一个类的对象后,可以通过“对象.属性”的方式,对对象的属性进行赋值,这里,赋值操作要受到属性的数据类型和存储范围的制约,除此之外,没有其它制约条件,但是,在实际问题中,往往需要给属性赋值,加入额外的限制条件,这个条件就不能在属性声明时体现,只能通过方法进行限制条件的添加,这时需要避免用户再使用“对象.属性”的方式对属性进行赋值,则需要将属性声明为私有的 (private) --> 此时,针对属性就体现了封装性

封装性思想具体的代码体现
  • 将类的属性私有化 (private),同时提供公共的 (public) 方法来获取 (getXxx) 和设置 (setXxx) 值

    public class Circle{
    
        private double radius;
    
        public void setRadius(double radius){
            this.radius  = radius;
        }
    
        public double getRadius(){
            return radius;
        }
    }
    
  • 不对外暴露的私有方法

  • 单例模式(将构造器私有化)

  • 如果不希望类在包外被调用,可以将类设置为缺省的

Java 规定的四种权限修饰符
权限从小到大顺序

private < 缺省 < protected < public

具体的修饰范围
修饰符类内部同包不同包的子类同一个过程
privateyes
缺省yesyes
protectedyesyesyes
publicyesyesyesyes
权限修饰符可用来修饰的结构说明

具体的,4种权限可以用来修饰类及类的内部结构:属性、方法、构造器、内部类

  • 修饰类的话,只能使用:缺省 public
    

类的结构:构造器 Constructor

作用
  1. 创建对象
  2. 初始化对象的信息
使用说明
  1. 如果没有显示的定义类的构造器的话,则系统默认提供一个空参的构造器

  2. 定义构造器的格式:权限修饰符 类名(形参列表){}

  3. 一个类中定义的多个构造器,披此构成重载

  4. 一旦显示地定义了类的构造器之后,系统就不再提供默认的空参构造器

  5. 一个类中,至少会有一个构造器

举例
class Person{
	String name;
	int age;

	// 构造器
	public Person() {
		System.out.println("Person()..........");
	}

	public Person(String n) {
		name = n;
	}

	public Person(String n, int a) {
		name = n;
		age = a;
	}

	public void eat() {
		System.out.println("人吃饭");
	}

	public void study() {
		System.out.println("人可以学习");
	}
}
属性的赋值顺序

总结:属性赋值的先后顺序

  1. 默认初始化

  2. 显示初始化

  3. 构造器中初始化

  4. 通过“对象.方法”或“对象.属性”的方式赋值

以上操作的先后顺序:1 --> 2 --> 3 --> 4

JavaBean 的概念

所谓 JavaBean,是指符合如下标准的 Java 类:

  • 类是公共的
  • 有一个无参的公共的构造器
  • 有属性,且有对应的 get、set 方法

this 关键字

可以调用的结构
  • 属性、方法
  • 构造器
调用属性、方法

this 理解为:当前对象或当前正在创建的对象

  1. 在类的方法中,可以使用“this.属性”或“this.方法”的方式,调用当前对象的属性或方法。但是,通常情况下,都选择省略"this.“,特殊情况下,如果方法的形参和类的属性同名时,必须显示地使用“this.变量”的方式,表明此变量是属性,而非形参

  2. 在类的构造器中,可以使用“this.属性”或“this.方法”的方式,调用当前正在创建的对象的属性或和方法。但是,通常情况下,都选择省略"this.“,特殊情况下,如果构造器的形参和类的属性同名时,必须显示地使用“this.变量”的方式,表明此变量是属性,而非形参

调用构造器
  1. 在类的构造器中,可以显示地使用“this(形参列表)”方式,调用本类中指定的其它的构造器

  2. 构造器中不能通过“this(形参列表)”方式调用自己

  3. 如果一个类中有 n 个构造器,则最多有 n - 1 个构造器中使用了“this(形参列表)”

  4. 规定:“this(形参列表)”必须声明在当前构造器的首行

  5. 构造器内部,最多只能声明一个“this(形参列表)”方式,用来调用其它构造器

关键字 package / import

package 关键字的使用
使用说明
  1. 为了更好的实现项目中类的管理,提供包的概念

  2. 使用 package 声明类或接口所属的包,声明在源文件的首行

  3. 包,属于标识符,遵循命名规则、规范,见名知义

  4. 每“."一次,就代表一层文件目录

举例

举例一:

image-20211107121611133

举例二:MVC 设计模式

image-20211107121847355

JDK 中的主要包介绍
  1. java.lang----包含一些Java语言的核心类,如String、Math、Integer、 System和 Thread,提供常用功能
  2. java.net----包含执行与网络相关的操作的类和接口。
  3. java.io ----包含能提供多种输入/输出功能的类。
  4. java.util----包含一些实用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关的函数。
  5. java.text----包含了一些java格式化相关的类
  6. java.sql----包含了java进行JDBC数据库编程的相关类/接口
  7. java.awt----包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。 B/S C/S
import 关键字的使用

import:导入

  1. 在源文件中显示使用 import 结构导入指定包下的类和接口
  2. 声明在包的声明和类的声明之间
  3. 如果使用导入多个结构,则并列写出即可
  4. 可以使用“xxx.*”的方式,表示可以导入 xxx 包下的所有结构
  5. 如果使用的类或接口是 java.lang 包下定义的,则可以省略 import 结构
  6. 如果使用的类或接口是本包下定义的,则可以省略 import 结构
  7. 如果在源文件中,使用了不同包下的同名的类,则必须至少有一个类需要以全类名的方式显示
  8. 使用“xxx.*”的方式表明可以调用 xxx 包下的所有结构,但是如果的是 xxx 子包下的结构,则仍需要显示导入
  9. import static:导入指定类或接口中的静态结构:属性或方法

面向对象的特征二:继承性

为什么要有继承性(继承性的好处)
  1. 减少了代码冗余,提高了代码复用性

  2. 便于功能的扩展

  3. 为之后的多态性的使用,提供了前提

image-20211108165351596

继承性的格式
class A extends B{

}
  • A:子类、派生类、subClass
  • B:父类、超类、基类、superClass
子类继承父类之后有哪些不同
  1. 体现:一旦子类 A 继承父类 B 以后,子类 A 中就获取了父类 B 中声明的所有属性和方法,特别的,父类中声明为 private 的属性和方法,子类继承父类以后,仍然认为获取了父类中私有的结构,只有因为封装性的影响,使得子类不能直接调用父类的结构而已

  2. 子类继承父类以后,还可以声明自己特有的属性或方法:实现功能的扩展子类和父类的关系,不同于子集和集合的关系extends:延展、扩展

Java 中继承性的说明
  1. 一个类可以被多个子类继承

  2. Java 中类的单继承性:一个类只能有一个父类

  3. 子父类是相对的概念

  4. 子类直接继承的父类,称为直接父类,间接继承的父类,称为间接父类

  5. 子类继承父类以后,就获取了直接父类以及所有间接父类中声明的属性和方法

image-20211108170022233

java.lang.Object 类的理解
  1. 如果没有显示声明一个类的父类的话,则此类继承于 java.lang.Object
  2. 所有的 Java 类(除 java.lang.Object 类之外)都直接或间接地继承于 java.lang.Object
  3. 所有的 Java 类具有 java.lang.Object 类声明的功能

方法的重写

什么是方法的重写(override 或 overwrite)?

子类继承父类以后,可以对父类中同名同参数的方法,进行覆盖操作

应用

重写以后,当创建子类对象以后,通过子类对象调用子父类中的同名同参数的方法时,实际执行的是子类重写父类的方法

举例
class Circle{
    /**
     * 求面积
     */
    public double findArea(){}
}

class Cylinder extends Circle{
    /**
     * 求表面积
     */
    public double findArea(){}
}

class Account{
    public boolean withdraw(double amt){}
}

class CheckAccount extends Account{
    public boolean withdraw(double amt){}
}
重写的规则

方法的声明:

权限修饰符 返回值类型 方法名 (形参列表) throws 异常的类型 {
    方法体
}

约定俗称:子类中的叫重写方法,父类中的叫被重写的方法

  1. 子类重写的方法的方法名与形参列表与父类被重写的方法的方法名与形参列表相同

  2. 子类重写的方法的权限修饰符不小于父类被重写的方法权限修饰符。特殊情况:子类不能重写父类中声明为 private 的方法

  3. 子类重写的方法的返回值类型

    1. 父类被重写的方法的返回值类型是 void,则子类重写的方法的返回值类型只能是 void

    2. 父类被重写的方法的返回值类型是 A,则子类重写的方法的返回值类型可以是 A 类及其子类

    3. 父类被重写的方法的返回值类型是基本数据类型,则子类重写的方法的返回值类型必须是相同的基本数据类型

  4. 子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型

子类和父类中的同名同参数的方法要么都声明为非 static 的(考虑重写), 要么都声明为非 static 的(不是重写)

面试题

如何区分方法的重写与重载?

  1. 二者的概念

  2. 重载和重写的具体规则

  3. 重载不表现为多态性,重写表现为多态性

    重载,是指允许存在多个同名方法,而这些方法的参数不同。编译器根据方法不 同的参数表,对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了 不同的方法。它们的调用地址在编译期就绑定了。Java的重载是可以包括父类 和子类的,即子类可以重载父类的同名不同参数的方法。 所以:对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法, 这称为“早绑定”或“静态绑定”;

    而对于多态,只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体 方法,这称为“晚绑定”或“动态绑定”。

    引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”

super 关键字的使用

super 理解

父类的

super 调用的结构
  • 属性
  • 方法
  • 构造器
super 调用属性和方法
  1. 可以在子类的方法或构造器中,通过使用“super.属性”或“super.方法”的方式,显示的调用父类中声明的属性或方法,但是,通常情况下,可以省略“super.”
  2. 特殊情况,当子类和父类中定义了同名的属性时,想在子类中定义父类中声明的属性,则必须显示地使用“super.属性”的方式,表明调用的是父类中声明的属性
  3. 特殊情况,当子类重写了父类的方法以后,想在子类的方法中定义父类中被重写的方法时,则必须显示地使用“super.方法”的方式,表明调用的是父类中被重写的方法
super 调用构造器
  1. 可以在子类的构造器中显示的使用“super(形参列表)”的方式,调用父类中声明的指定构造器
  2. “super(形参列表)”的使用,必须声明在子类构造器的首行
  3. 在类的构造器中,针对于 “this(形参列表)”或“super(形参列表)”只能二选一,不能同时出
  4. 在构造器的首行,没有显示声明 “this(形参列表)”或“super(形参列表)”,则默认调用的是父类中的空参构造器
  5. 在类的多个构造器中,至少有一个构造器中使用了 “super(形参列表)”,调用父类中的构造器

子类对象实例化全过程

从结果上看:(继承性)

子类继承父类以后,就获取了父中声明的属性和方法

创建子类的对象,在堆空间中,就会加载所有父类中声明的属性

从过程上看

当通过子类的构造器创建子类对象时,一定会直接和间接地调用其父类的构造器,进而调用父类的父类的构造器,直到调用了 java.lang.Object 类中的空参构造器为止,正因为加载过所有的父类的结构,所以才可以看到内存中有父类指定结构,子类对象才可以考虑进行调用

image-20211109205623331

强调说明

虽然创建子类对象时,调用了父类构造器,但是自始至终就创建过一个对象,即为 new 出来的子类对象

image-20211109212346843

面向对象的特征之三:多态性

理解多态性

可以理解为一个事物的多种形态

何为多态性

对象的多态性:父类的引用指向子类的对象(或子类对象赋给父类引用)

举例

Person p = new Man();
Object obj = new Date();
多态的使用:虚拟方法调用

有了对象的多态性以后,在编译期,只能调用父类中声明的方法,但在运行期,实际执行的是子类重写父类的方法总结:编译看左边,运行看右边

多态性的使用前提
  • 类的继承关系
  • 方法的重写
多态性的应用举例
// 举例一:
public class AnimalTest {

	public static void main(String[] args) {
		AnimalTest test = new AnimalTest();
		test.func(new Dog());
		test.func(new Cat());
	}

	public void func(Animal animal) {
		animal.eat();
		animal.shout();
	}
}

class Animal{

	public void eat() {
		System.out.println("动物进食");
	}

	public void shout() {
		System.out.println("动物叫");
	}
}

class Dog extends Animal{
	@Override
	public void eat() {
		System.out.println("狗吃骨头");
	}

	@Override
	public void shout() {
		System.out.println("汪汪汪");
	}
}

class Cat extends Animal{
	@Override
	public void eat() {
		System.out.println("猫吃鱼");
	}

	@Override
	public void shout() {
		System.out.println("喵喵喵");
	}
}

//举例二:
class Order{
	public void method(Object obj) {

	}
}

// 举例三:
class Driver{
	public void doData(Connection conn) {
		// 规范的步骤去操作数据
	}
}
多态性使用的注意点

对象的多态性,只适用于方法,不适用于属性(编译和运行都看左边)

向上转型和向下转型
向上转型

多态

向下转型

为什么要使用向下转型:有了对象多态性以后,内存中实际上是加载了子类特有的属性和方法,但是由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性和方法,子类特有的属性和方法不能调用, 如何才能调用子类特有的属性和方法?使用向下转型

如何实现向下转型:使用强制类型转换符

注意点

  • 使用强转时,可能出现 ClassCastException 异常
  • 为了避免在向下转型时出现 ClassCastException 的异常,在向下转型之前,先进行 instanceof 的判断,一旦返回 true,就进行向下转型,返回 false,不进行向下转型
instanceof 关键字的使用
  • a instanceof A 判断 a 是否是类 A 的实例,如果是,返回 true,如果不是,返回 false
  • 如果 a instanceof A 返回 true,则 a instanceof B 返回 true,其中类 B 是类 A 的父类
  • a instanceof A 要求 a 所属的类与类 A 必须是子类和父类的关系,否则编译错误

image-20211111102748626

面试题
谈谈你对多态性的理解?
  • 实现代码的通用性

  • Object 类中定义的 public boolean equals(Object obj){}

    JDBC:使用 Java 程序操作(获取数据库连接、CRUD)数据库(MySQL、Oracle、DB2、SQL Server)

  • 抽象类和接口的使用肯定体现了多态性(抽象类和接口不能实例化)

多态是编译时行为还是运行时行为?

运行时行为

证明

package com.atguigu.test;

import java.util.Random;

//面试题:多态是编译时行为还是运行时行为?
//证明如下:
class Animal  {
 
	protected void eat() {
		System.out.println("animal eat food");
	}
}

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

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

class Sheep  extends Animal  {

	public void eat() {
		System.out.println("Sheep eat grass");
	}
}

public class InterviewTest {

	public static Animal  getInstance(int key) {
		switch (key) {
		case 0:
			return new Cat ();
		case 1:
			return new Dog ();
		default:
			return new Sheep ();
		}
	}

	public static void main(String[] args) {
		int key = new Random().nextInt(3);
		System.out.println(key);
		Animal  animal = getInstance(key);
		animal.eat();
	}
}

Object 类的使用

java.lang.Object 类的说明
  1. Object 类是所有 Java 类的根父类

  2. 如果在类的声明中未使用 extends 关键字指明其父类,则默认父类为 java.lang.Object

  3. Object 类中的功能(属性、方法)就具有通用性

    • 属性:无

    • 方法:equals() toString() getClass() hashCode() clone() finalize() wait() notify() notifyAll()

  4. Object 类只声明了一个空参构造器

equals() 方法
使用
  1. 是一个方法,而非运算符

  2. 只能适用于引用数据类型

  3. Object 类中 equals() 的定义

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

    说明:Object 类中定义的 equals() 方法和 == 的作用是相同的,比较两个地址值是否相同,即两个引用是否指向同一个对象实体

  4. 像 String、Date、File、包装类等都重写了 Object 类中的 equals() 方法,重写以后,比较的表示两个引用的地址是否相同,而是比较两个对象的“实体内容”是否相同

  5. 通常情况下,自定义的类如果使用 equals() 的话,也通常比较两个对象的“实体内容”是否相同,那么,就需要对 Object 的 equals() 进行重写,重写的规则:比较两个对象的实体内容是否相同

如何重写 equals()

手动重写举例

public class Customer {
	
	String name;
	int age;
	
	/**
	 * 重写的规则:比较两个对象的实体内容是否相同
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj instanceof Customer) {
			Customer cust = (Customer) obj;
			// 比较两个对象的属性是否都相同
			if (this.age == cust.getAge() && this.name.equals(cust.getName())) {
				return true;
			}
			return this.age == cust.getAge() && this.name.equals(cust.getName());
		}
		return false;
	}
}

开发中如何实现:自动生成的

public class Customer {
	
	String name;
	int age;
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Customer other = (Customer) obj;
		return age == other.age && Objects.equals(name, other.name);
	}
}

回顾 == 运算符的使用

==:运算符

  1. 可以使用在基本数据类型变量和引用数据类型变量中

  2. 如果比较的是基本数据类型变量,比较的是两个变量保存的数据是否相等,不一定类型要相同

    如果比较的是引用数据类型变量,比较的是两个地址值是否相同,即两个引用是否指向同一个对象实体

补充: == 符号使用时,必须保证符号左右两边的变量类型一致

toString() 方法
使用
  1. 当输出一个对象的引用时,实际上是调用当前对象的 toString()

  2. Object 类中 toString() 的定义

public String toString() {
	return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
  1. 像 String、Date、File、包装类等都重写了 Object 类中的 toString() 方法,使得在调用对象的 toString() 方法时,返回对象“实体内容”信息

  2. 自定义类也可以重写 toString() 方法,当调用此方法时,返回对象的“实体内容”

如何重写 toString()

举例

@Override
public String toString() {
    return "Customer [name=" + name + ", age=" + age + "]";
}
面试题
  1. final、finally、finalize 的区别
  2. == 和 equals() 的区别

单元测试方法

  • 步骤

    • 选中当前过程 - 右键选择:build path - add libraries - Junit 5 - 下一步

    • 创建 Java 类,进行单元测试

      此时的 Java 类要求

      • 此类是 public 的
      • 此类提供公共的无参构造器
    • 此类中声明单元测试方法

      此时的单元测试方法:方法的权限是 public ,没有返回值,没有形参

    • 此单元测试方法上需要声明注解 @Test,并在单元测试类中导入:import org.junit.jupiter.api.Test;

    • 声明号单元测试方法以后,就可以在方法体内写相关代码

    • 写好代码以后,右键单击单元测试方法名,右键 run as - JUnit Test

说明:

  • 如果执行结构没有任何异常,绿条
  • 如果执行结构出现异常,红条

包装类的使用

为什么要有包装类(或封装器)

为了使基本数据类型的变量具有类的特征,引入包装类

基本数据类型与对应的包装类
基本数据类型包装类
intInteger
byteByte
shortShort
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean
需要掌握的类型间的转换

基本数据类型、包装类和 String 类型

image-20211111153026535

简易版
  • 基本数据类型 <—> 包装类:JDK 5.0 新特性:自动装箱与自动拆箱

  • 基本数据类型、包装类 --> String:调用 String 重载的 valueOf(Xxx xxx)

  • String --> 基本数据类型、包装类:调用包装类的 parseXxx(String s)

注意:转换时,可能会报 NumberFormatException

应用场景举例
  • Vector 类中关于添加元素,只定义了形参为 Object 类型的方法

    v.addElement(Object obj); // 基本数据类型 --> 包装类 --> 使用多态
    

static 关键字的使用

static:静态的

可以用来修饰

主要用来修饰类的内部结构

  • 属性
  • 方法
  • 代码块
  • 内部类
static 修饰属性

静态变量(类变量)

  1. 属性:按是否使用 static 修饰,又分为:静态属性 VS 非静态属性(实例变量)

    实例变量:创建了类的多个对象,每个对象都独立的拥有一套类中的非静态属性,当修改其中一个对象中的非静态属性时,不会导致其它对象中同样的属性值的修改

    静态变量:创建了类的多个对象,每个对象都共享同一个静态变量,当通过某一个对象修改静态变量时,会导致其它对象调用此静态变量时,是修改过了的

  2. 其它说明

    1. 静态变量随着类的加载而加载,可以通过“类.静态变量”的方式进行调用

    2. 静态变量的加载早于对象的创建

    3. 由于类只会加载一次,则静态变量在内存中也只会存在一份,存在方法区的静态域中

      静态方法非静态方法
      yesno
      对象yesyes
  3. 静态属性举例:System.out, Math.PI

静态变量内存解析

image-20211112173238836

static 修饰方法

静态方法、类方法

  1. 随着类的加载而加载,可以通过“类.静态方法”的方式进行调用

    静态方法非静态方法
    yesno
    对象yesyes
  2. 静态方法中,只能调用静态的方法或属性

    非静态方法中,即可以调用静态的方法或属性,也可以调用非静态的方法或属性

static 注意点
  1. 在静态方法中,不能使用 this、super 关键字
  2. 关于静态属性和静态方法的使用,都从生命周期的角度去理解
如何判定属性和方法应该使用 static 关键字
  1. 关于属性

    1. 属性是可以被多个对象所共享的,不会随着对象的不同而不同的
    2. 类中的常量也常常声明为 static 的
  2. 关于方法

    1. 操作静态属性的方法,通常设置为 static 的
    2. 工具类中的方法,习惯上声明为 static 的,比如:Arrays、Collections、Math
使用举例
  • 举例一:Arrays、Collections、Math 等工具类

  • 举例二:单例模式

  • 举例三:

    class Circle{
    	
    	private double radius;
    	
    	/**
    	 * 自动赋值
    	 */
    	private int id;
    	
    	/**
    	 * 记录创建的圆的个数
    	 */
    	private static int total;
    	
    	/**
    	 * static 声明的属性被所有对象共享
    	 */
    	private static int init = 1001;
    	
    	public Circle() {
    		id = init++;
    		total++;
    	}
    	
    	public Circle(double radius) {
    		this();
    		this.radius = radius;
    	}
    	
    	public static int getTotal() {
    		return total;
    	}
    
    	public double getRadius() {
    		return radius;
    	}
    
    	public void setRadius(double radius) {
    		this.radius = radius;
    	}
    
    	public int getId() {
    		return id;
    	}
    
    	public double findArea() {
    		return Math.PI * radius * radius;
    	}
    }
    

单例模式

设计模式的说明
理解

设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、 以及解决问题的思考方式。

常用设计模式

23种经典的设计模式 GoF

  • 创建者模式
    • 工厂方法模式
    • 抽象工厂模式
    • 单例模式
    • 建造者模式
    • 原型模式
  • 结构型模式
    • 适配器模式
    • 装饰器模式
    • 代理模式
    • 桥接模式
    • 组合模式
    • 享元模式
  • 行为型模式
    • 策略模式
    • 模板方法模式
    • 观察者模式
    • 迭代器模式
    • 责任链模式
    • 命令模式
    • 状态模式
    • 访问者模式
    • 中介者模式
    • 解释器模式
单例模式
要解决的问题

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例

具体代码的实现

饿汉式1

class Bank{
	
	// 1.私有化类的构造器
	private Bank() {
		
	}
	
	// 2.内部创建类的对象
	// 4.要求此对象也必须声明为静态的
	private static Bank instance = new Bank();
	
	// 3.提供公共的静态方法,返回类的对象
	public static Bank getInstance() {
		return instance;
	}
}

饿汉式2

class Bank{
	
	// 1.私有化类的构造器
	private Bank() {
		
	}
	
	// 2.内部创建类的对象
	// 4.要求此对象也必须声明为静态的
	private static Bank instance;
    
    static{
        instance = new Bank();
    }
	
	// 3.提供公共的静态方法,返回类的对象
	public static Bank getInstance() {
		return instance;
	}
}

懒汉式

class Order{
	
	// 1.私有化类的构造器
	private Order() {
	}
	
	// 2.声明当前类对象,没有实例化
	// 4.此对象必须声明为 static 的
	private static Order instance = null;
	
	// 3.声明 public static 的返回当前类对象的方法
	public static Order getInstance() {
		if (instance == null) {
			instance = new Order();
		}
		return instance;
	}
}
两种方式的对比
  • 饿汉式

    •    坏处:对象加载时间过长
      
    •    好处:饿汉式是线程安全的
      
  •    懒汉式
       *        好处:延迟对象的创建
       *        目前的写法坏处:线程不安全 --> 到多线程内容时,再修改
    

main() 的使用说明

  • main() 方法作为程序的入口

  • main() 方法是一个普通的静态方法

  • main() 方法可以作为我们与控制台交互的方式(之前,使用 Scanner)

    如何将控制台获取的数据传给形参:String[] args?

    运行时:java 类名 “Tom” “123” “true” “Jerry”

    sysout(args[0]); // "Tom"
    sysout(args[2]); // "true"
    sysout(args[4]); // 报异常
    

    小结

    public static void main(String[] args){
        // 方法体
    }
    
    • 权限修饰符:private 缺省 protected public —-> 封装性
    • 修饰符:static final abstract native 可以用来修饰方法
    • 返回值类型:无返回值 有返回值 --> return
    • 方法名:需要满足标识符命名的规则、规范,见名知义
    • 形参列表:重载 VS 重写;参数的值传递机制;体现对象的多态性
    • 方法体:体现方法的功能

类的结构之四:代码块

又叫初始化块

代码块的作用

用来初始化类和对象的信息

分类

代码块如果有修饰的话,只能使用 static

静态代码块 VS 非静态代码块

  1. 静态代码块
    1. 内部可以有输出语句
    2. 随着类的加载而执行,而且只执行一次
    3. 作用:初始化类的信息
    4. 如果一个类中定义了多个静态代码块,则按照声明的先后顺序执行
    5. 静态代码块的执行优先于非静态代码块的执行
    6. 静态代码块内只能调用静态的属性、静态的方法,不能调用非静态结构
  2. 非静态代码块
    1. 内部可以有输出语句
    2. 随着对象的创建而执行
    3. 每创建一个对象,就执行一次非静态代码块
    4. 作用:可以在创建对象时,对对象的属性进行初始化
    5. 如果一个类中定义了多个非静态代码块,则按照声明的先后顺序执行
    6. 非静态代码块内可以调用静态的属性、静态的方法,或非静态的属性、非静态的方法
实例化子类对象的加载顺序

涉及到父类、子类中静态代码块、非静态代码块构造器的加载顺序:由父及子,静态先行

LeafTest.java

package cn.tedu.java3;

class Root{
	static{
		System.out.println("Root 的静态初始化块");
	}
	
	{
		System.out.println("Root 的普通初始化块");
	}
	
	public Root(){
		System.out.println("Root 的无参数的构造器");
	}
}

class Mid extends Root{
	static{
		System.out.println("Mid 的静态初始化块");
	}
	
	{
		System.out.println("Mid 的普通初始化块");
	}
	
	public Mid(){
		System.out.println("Mid 的无参数的构造器");
	}
	
	public Mid(String msg){
		// 通过 this 调用同一类中重载的构造器
		this();
		System.out.println("Mid 的带参数构造器,其参数值:" + msg);
	}
}

class Leaf extends Mid{
	static{
		System.out.println("Leaf 的静态初始化块");
	}
	
	{
		System.out.println("Leaf 的普通初始化块");
	}	
	
	public Leaf(){
		// 通过 super 调用父类中有一个字符串参数的构造器
		super("尚硅谷");
		System.out.println("Leaf 的构造器");
	}
}

public class LeafTest{
	public static void main(String[] args){
		new Leaf(); 
		new Leaf();
	}
}

Son.java

package cn.tedu.java3;


class Father {
	static {
		System.out.println("11111111111");
	}
	{
		System.out.println("22222222222");
	}

	public Father() {
		System.out.println("33333333333");

	}

}

public class Son extends Father {
	static {
		System.out.println("44444444444");
	}
	{
		System.out.println("55555555555");
	}
	public Son() {
		System.out.println("66666666666");
	}


	public static void main(String[] args) { // 由父及子 静态先行
		System.out.println("77777777777");
		System.out.println("************************");
		new Son();
		System.out.println("************************");

		new Son();
		System.out.println("************************");
		new Father();
	}

}

属性的赋值顺序
  1. 默认初始化

  2. 显示初始化

  3. 对象初始化

  4. 有了对象以后,可以通过“对象.属性”或“对象.方法”的方式,进行赋值

  5. 代码块赋值

执行的先后顺序: 1 - 2 / 5 - 3 - 4

final 关键字的使用

可以用来修饰的结构

类、方法、变量

用法
  1. final 用来修饰一个类:此类不能被其它类所继承,比如:String 类、System 类、StringBuffer 类

  2. final 用来修饰方法:表明此方法不可以被重写,比如:Object 类的 getClass();

  3. final 用来修饰变量:此时的“变量”就称为一个常量

    1. final 修饰一个属性,可以考虑赋值的位置有:显示初始化、代码块中初始化、构造器中赋值
    2. final 修饰局部变量尤其使用 final 修饰形参时,表明此形参是一个常量,当调用此方法时,给常量形参赋一个实参,一旦赋值以后,就只能在方法体内使用此形参,但不能进行重新赋值
  4. static final 用来修饰属性,全局常量

abstract 关键字的使用

abstract:抽象的

可以用来修饰的结构

类、方法

具体的
  1. abstract 修饰类:抽象类

    1. 此类不能实例化
    2. 抽象类一定有构造器,便于子类实例化时调用(涉及,子类对象实例化全过程)
    3. 开发中,都会提供抽象类的子类,完成相关的功能
  2. abstract 修饰方法:

    1. 抽象方法抽象方法只有方法的声明,没有方法体

    2. 包含抽象方法的类,一定是一个抽象类,反之,抽象类中可以没有抽象方法

    3. 若子类重写了父类中的所有抽象方法后,此子类方可实例化

      若子类没有重写了父类中的所有抽象方法后,此子类也是一个抽象类,需要使用 abstract 修饰

使用上的注意点
  1. abstract 不能用来修饰属性、构造器等结构
  2. abstract 不能用来修饰私有方法、静态方法、final 的方法、final 的类
abstract 的应用举例
  • 举例一

    public abstract class Vehicle{
        public abstract double calcFuelEfficiency(); // 计算燃料效率的抽象方法
        public abstract double calcTripDistance();	// 计算行驶距离的抽象方法
    }
    public class Truck extends Vehicle{
        public double calcFuelEfficiency(){ 
            //写出计算卡车的燃料效率的具体方法	
        }
    
        public double calcTripDistance(){  
            //写出计算卡车行驶距离的具体方法	
        }
    }
    
    public class RiverBarge extends Vehicle{
        public double calcFuelEfficiency() { 
            //写出计算驳船的燃料效率的具体方法 
        } 
    
        public double calcTripDistance()  {  
            //写出计算驳船行驶距离的具体方法
        }
    }
    
  • 举例二:

    public class Circle extends GeometricObject{
    
    	private double radius;
        
        @Override
    	public double findArea() {
    		return Math.PI * radius * radius;
    	}
    }
    
    abstract class GeometricObject {
    	
    	public abstract double findArea();
    }
    
    
  • 举例三

    IO 流中涉及到的抽象类:InputStream / OutputStream / Reader / Writer,在其内部定义了 read() / write()

模板方法设计模式
解决的问题

在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽 象出来,供不同子类实现。这就是一种模板模式。

举例
package cn.tedu.java;

public class TemplateTest {
	public static void main(String[] args) {
		Template template = new SubTemplate();
		template.spendTime();
	}
}

abstract class Template{
	
	/**
	 * 计算某段代码所花费的时间
	 */
	public void spendTime() {
		long start = System.currentTimeMillis();
		// 易变的部分
		code();
		long end = System.currentTimeMillis();
		System.out.println("花费的时间为:" + (end - start));
	}
	
	public abstract void code();
}

class SubTemplate extends Template{

	@Override
	public void code() {
		for(int i  = 2; i < 10000000; i++) {
			boolean isFlag = true;
			for(int j = 2; j <= Math.sqrt(i); j++) {
				if (i % j == 0) {
					isFlag = false;
					break;
				}
			}
			if (isFlag) {
				System.out.println(i);
			}
		}
	}	
}

应用场景
  • 数据库访问的封装
  • Junit 单元测试
  • JavaWeb 的 Servlet 中关于 doGet / doPost 方法调用
  • Hibernate 中模板程序
  • Spring 中 JDBCTemlate、HibernateTemplate 等

interface 关键字的使用

使用说明
  1. 接口使用 interface 定义

  2. Java 中,接口和类是并列的两个结构

  3. 如何定义接口:定义接口是成员

    1. JDK7及以前:只能定义全局常量和抽象方法

      全局常量:public static final,但是书写时,可以省略不写

      抽象方法:public abstract,但是书写时,可以省略不写

    2. JDK8:除了定义全局常量和抽象方法之外,还可以定义静态方法和默认方法

  4. 接口中不能定义构造器,意味着接口不可以实例化

  5. Java 开发中,接口通过类实现(implements)的方式来使用

    如果实现类覆盖了接口中所有的抽象方法,则此实现类就可以实例化

    如果实现类没有覆盖接口中所有的抽象方法,则此实现类仍为一个抽象类

  6. Java 类可以实现多个接口 --> 弥补了 Java 单继承的局限性

    格式:class AA extends BB implements CC, DD, EE {}

  7. 接口与接口之间可以继承,而且可以多继承

  8. 接口的具体使用,体现多态性

  9. 接口实际上可以看做是一种规范

举例

image-20211114151429850

package cn.tedu.java1;

public class USBTest {

	public static void main(String[] args) {
		Computer com = new Computer();
		// 1.创建了接口的非匿名实现的非匿名对象
		Flash flash = new Flash();
		com.transforData(flash);
		// 2.创建了接口的非匿名实现的匿名对象
		com.transforData(new Printer());
		// 3.创建了接口的匿名实现的非匿名对象
		USB phone = new USB() {
			
			@Override
			public void stop() {
				System.out.println("手机结束工作");
			}
			
			@Override
			public void start() {
				System.out.println("手机开始工作");
			}
		};
		com.transforData(phone);
		// 4.创建了接口的匿名实现的匿名对象
		com.transforData(new USB() {
			
			@Override
			public void stop() {
				System.out.println("mp3 结束工作");
			}
			
			@Override
			public void start() {
				System.out.println("mp3 开始工作");
			}
		});
	}
}

class Computer{
	
	public void transforData(USB usb) {
		usb.start();
		System.out.println("具体传输数据的细节");
		usb.stop();
	}
}

interface USB{
	
	void start();
	
	void stop();
}

class Flash implements USB{

	@Override
	public void start() {
		System.out.println("U盘开启工作");
	}

	@Override
	public void stop() {
		System.out.println("U盘结束工作");
	}
	
}

class Printer implements USB{

	@Override
	public void start() {
		System.out.println("打印机开启工作");
	}

	@Override
	public void stop() {
		System.out.println("打印机结束工作");
	}
	
}

体会

  • 接口使用上满足多态性

  • 接口实际上定义了一种规范

  • 开发中,体会面向接口编程

体会面向接口编程的思想

image-20211114151917892

面向接口编程,在应用程序中,调用的结构都是 JDBC 中定义的接口,不会出现具体某一个数据库厂商的 API

JDK8 中关于接口的新规范
  • 接口中定义的静态方法,只能通过接口来调用

  • 通过实现类对象,可以调用接口中的默认方法如果实现类重写了接口中的默认方法,调用时,仍然调用的是重写以后的方法

  • 如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的方法,那么子类在没有重写此方法的情况下,默认调用的是父类中的同名同参数的方法 --> 类优先原则

  • 如果实现类实现了多个接口,而这多个接口中定义了同名同参数的默认方法,那么在实现类没有重写此方法的情况下,报错 --> 接口冲突,这就需要我们必须在实现类中重写此方法

  • 如何在子类(或实现类)的方法中调用父类、接口中被重写的方法

    public void myMethod() {
        method3(); // 调用自己定义的重写的方法
        super.method3(); // 调用父类中声明的
        // 调用接口中的默认方法
        CompareA.super.method3();
        CompareB.super.method3();
    }
    
面试题
  • 抽象类和接口有哪些异同?

    • 相同点

      • 不能实例化
      • 都可以包含抽象方法
    • 不同点

      • 把抽象类和接口的定义、内部结构解释说明

      • 类:单继承

        接口:多继承

        类与接口:多实现

代理模式
解决的问题

代理模式是 Java 开发中使用较多的一种设计模式。代理设计就是为其 他对象提供一种代理以控制对这个对象的访问。

举例
public class NetWorkTest {
	public static void main(String[] args) {
		Server server = new Server();
		ProxyServer proxyServer = new ProxyServer(server);
		proxyServer.browse();
	}
}

interface NetWork{
	
	void browse();
}

// 被代理类
class Server implements NetWork{
	@Override
	public void browse() {
		System.out.println("真实的服务器访问网络");
	}
}

// 代理类
class ProxyServer implements NetWork{
	
	private NetWork work;
	
	public ProxyServer(NetWork work) {
		this.work = work;
	}
	
	@Override
	public void browse() {
		check();
		work.browse();
	}
	
	public void check() {
		System.out.println("联网之前的检查工作");
	}
}

应用场景
  • 应用场景:
    • 安全代理:屏蔽对真实角色的直接访问。
    • 远程代理:通过代理类处理远程方法调用(RMI)
    • 延迟加载:先加载轻量级的代理对象,真正需要再加载真实对象

比如你要开发一个大文档查看软件,大文档中有大的图片,有可能一个图片有100MB,在打开文件时,不可能将所有的图片都显示出来,这样就可以使用代理 模式,当需要查看图片时,用 proxy 来进行大图片的打开。

  • 分类
    • 静态代理(静态定义代理类)
    • 动态代理(动态生成代理类)
      • JDK 自带的动态代理,需要反射等知识
工厂的设计模式
解决的问题

实现了创建者与调用者的分离,即将创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的。

具体模式
  • **简单工厂模式:**用来生产同一等级结构中的任意产品。(对于增加新的产品, 需要修改已有代码)
  • **工厂方法模式:**用来生产同一等级结构中的固定产品。(支持增加任意产品)
  • **抽象工厂模式:**用来生产不同产品族的全部产品。(对于增加新的产品,无 能为力;支持增加产品族)

类的结构之五:内部类

定义

Java 中允许将一个类 A 声明在另一个类 B 中,则类 A 就是内部类,类 B 就是外部类

分类
  • 成员内部类(静态、非静态)
  • 局部内部类(方法内、代码块内、构造器内)
成员内部类的理解
  1. 一方面,作为外部类的成员
    1. 调用外部类的结构
    2. 可以被 static 修饰
    3. 可以被4种不同的权限修饰
  2. 另一方面,作为一个类
    1. 类内可以定义属性、方法、构造器等
    2. 可以被 final 修饰,表明此类不能被继承,言外之意,不使用 final,就可以被继承
    3. 可以被 abstract 修饰
局部内部类
  • 如何实例化成员内部类的对象

    public static void main(String[] args) {
    
        // 创建 Dog 实例(静态成员内部类)
        Dog dog = new Person.Dog();
    
        // 创建 Bird 实例(非静态成员内部类)
        Person p = new Person();
        Bird bird = p.new Bird();
    }
    
  • 如何在成员内部类中区分调用外部类的结构

    class Person{
    	
    	String name = "小明";
        
        public void eat(){
            
        }
        
    	// 非静态成员内部类
    	class Bird{
    		
    		String name = "杜鹃";
            
    		public void display(String name) {
    			System.out.println(name); // 方法形参
    			System.out.println(this.name); // 内部类属性
    			System.out.println(Person.this.name); // 外部类属性
                // Person.this.eat();
    		}
    	}
    }
    
局部内部类的使用
public class InnerClassTest1 {
	
	// 开发中很少见
	public void method() {
		class AA{
			
		}
	}
	
	/**
	 * 返回一个实现了 Comparable 接口的类对象
	 */
	public Comparable getComparable() {
		// 创建一个实现了 Comparable 接口的类:局部内部类
		/*class MyComparable implements Comparable{

			@Override
			public int compareTo(Object o) {
				// TODO Auto-generated method stub
				return 0;
			}
			
		}
		return new MyComparable();*/
		return new Comparable() {

			@Override
			public int compareTo(Object o) {
				// TODO Auto-generated method stub
				return 0;
			}
            
		};
	}
}

注意点

在局部内部类的方法中,如果调用局部内部类所声明的方法中的局部变量话,要求此局部变量声明为 final 的

  • JDK7 及之前版本:要求此局部变量显示的声明为 final 的
  • JDK8 及之后版本:可以省略 final 的声明
public class InnerClassTest {
    
	public void method() {
		// 局部变量
		int num = 10;
		
		class AA{
			
			public void show() {
				// num = 10;
				System.out.println(num);
			}
		}
	}
}
总结

成员内部类和局部内部类,在编译以后,都会生成字节码文件

格式

  • 成员内部类:外部类 $ 内部类名 .class
  • 局部内部类:外部类 $ 数字 内部类名 .class

Debug 调试

操作作用
step into 跳入(F5)进入当前行所调用的方法中
step over 跳过(F6)执行完当前行的语句,进入下一行
step return 跳回(F7)执行完当前行所在的的方法,进入下一行
drop to frame回到当前行所在方法的第一行
resume 恢复执行完当前行所在断点的所有代码,进入下一个断点,如果没有就结束
Terminate 停止停止 JVM,后面程序不会再执行

异常

异常

异常的体系结构
java.lang.Throwable
         |--- java.lang.Error:一般不编写针对性的代码进行处理
         |--- java.lang.Exception:可以进行异常的处理
                  |--- 编译时异常(checked)
                           |--- IOException
                                  |--- FileNotFoundException
                           |--- ClassNotFoundException
                  |--- 运行时异常(unchecked)
                  		   |--- NullPointerException
                           |--- ArrayIndexOutOfBoundsException
                           |--- ClassCastException
                           |--- NumberFormatException
                           |--- InputMismatchException
                           |--- ArithmeticException

image-20211114214908785

从程序的执行过程,看编译时异常和运行时异常

image-20211114215041132

编译时异常:执行 javac.exe 命令时,可能出现的异常

运行时异常:执行 java.exe 命令时,出现的异常

常见的异常类型,请举例说明
public class ExceptionTest {

	// NullPointerException
	@Test
	public void test1() {
		int[] arr = null;
		System.out.println(arr);
		String str = null;
		System.out.println(str.charAt(0));
	}
	
	// IndexOutOfBoundsException
	@Test
	public void test2() {
		// ArrayIndexOutOfBoundsException
		int[] a = new int[10];
		System.out.println(a[10]);
		// StringIndexOutOfBoundsException
		String str = "abc";
		System.out.println(str.charAt(3));
	}
	
	// ClassCastException
	@Test
	public void test3() {
		Object obj = new Date();
		String str = (String) obj;
	}
	
	// NumberFormatException
	@Test
	public void test4() {
		String str = "abc";
		int parseInt = Integer.parseInt(str);
	}
	
	// InputMismatchException
	@Test
	public void test5() {
		int score = new Scanner(System.in).nextInt();
		System.out.println(score);
	}
	
	// ArithmeticException
	@Test
	public void test6(){
		int a = 1 / 0;
	}
	
	//
//	@Test
//	public void test7() {
//		File file = new File("hello.txt");
//		FileInputStream fileInputStream = new FileInputStream(file);
//		int read  = fileInputStream.read();
//		while (read != -1) {
//			System.out.print((char)read);
//				read = fileInputStream.read();
//		}
//		fileInputStream.close();
//	}
}

异常的处理

Java 异常处理的抓抛模型
  1. “抛”,程序在正常执行过程中,一旦出现异常,就会在异常代码处生成一个对应异常类的对象,并将此对象抛出,一旦抛出异常以后,其后的代码就不再执行

    关于异常对象的产生:

    1. 系统自动生成的异常对象
    2. 手动生成一个异常对象,并抛出(throw)
  2. “抓”,可以理解为异常的处理方式:

    1. try-catch-finally
    2. throws
异常处理方式一:try-catch-finally
try{
    // 可能出现异常的代码
} catch(异常类型1 变量名1){
    // 处理异常的方式1
} catch(异常类型2 变量名2){
    // 处理异常的方式2
} catch(异常类型3 变量名3){
    // 处理异常的方式3
}
...
finally{
   // 一定会执行的代码
}
使用说明
  1. finally 是可选的

  2. 使用 try 将可能出现的异常代码包装起来,在执行过程中,一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,去 cache 中进行匹配

  3. 一旦 try 中的异常对象匹配到某一个 catch 时,就进入 catch 中进行异常处理,一旦处理完成,就跳出当前的 try-catch 结构(在没有写 finally 的情况),继续执行其后的代码

  4. catch 中的异常类型如果没有子父类关系,则谁声明在上,谁声明在下无所谓

    catch 中的异常类型如果满足子父类关系,则要求子类一定声明在父类的上面,否则报错

  5. 常用的异常对象处理的方式

    1. String getMessage()
    2. printStackTrace()
  6. 在 try 结构中声明的变量,出了 try 结构以后,就不能被调用

  7. try-catc-finally 结构可以相互嵌套

总结:如何看待代码中的编译时异常和运行时异常?

  1. 使用 try-catch-finally 处理编译时异常,是将程序在编译时就不再报错,但是运行时仍可能报错,相当于使用 try-catch-finally 将一个编译时可能出现的异常,延迟到运行时出现

  2. 开发中由于运行时异常比较常见,所以通常就不针对运行时异常编写 try-catch-finally,针对于编译时异常,一定要考虑异常的处理

finally 的再说明
  1. finally 是可选的
  2. finally 中声明的是一定会被执行的代码,即使 catch中又出现异常了,try 中有 return 语句,catch 中有 return 语句等情况
  3. 像数据库连接,输入输出流、网络编程 Socket 等资源,JVM 是不能自动回收,需要自己手动的进行资源的释放,此时的资源释放,就需要声明在 finally 中
面试题
  • final、finally、finalize 的区别

类似:

  • throw 和 throws
  • Collection 和 Collections
  • String、StringBuilder 和 StringBuffer
  • ArrayList 和 LinkedList
  • HashMap 和 LinkedHashMap
  • 重写和重载

结构不相似:

  • 抽象类和接口
  • == 和 equals()
  • sleep() 和 wait()

异常处理方式之二:throws

"throws + 异常类型"写在方法声明出,指明此方法执行时,可能会抛出的异常类型,一旦当方法体执行时,出现异常,仍然会在异常代码处生成一个异常类的对象。此对象满足 throws 后异常类型时,就会被抛出,异常代码后续的代码,就不再执行

对比两种处理方式

  • try-catch-finally:真正的将异常给异常处理掉了
  • throws 的方式注释将异常抛给了方法的调用者,并没有真正将异常处理掉

体会开发中应该如何选择两种处理方式

  1. 如果父类中被重写的方法没有 throws 方式处理异常,则子类重写的方法也不能使用 throws,意味着如果子类重写的方法有异常,必须使用 try-catch-finally 方式处理

  2. 执行的方法中先后又调用了另外的几个方法,这几个方法是递进关系执行的,建议这几个方法使用 throws的方式进行处理,而执行方法 a 可以考虑使用 try-catch-finally 方式进行处理

补充

方法重写的规则之一:子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型

手动抛出异常对象

使用说明

在程序执行中,除了自动抛出异常对象的情况之外,还可以手动 throw 一个异常类的对象

面试题
  • throw 和 throws 的区别
    • throw:表示抛出一个异常类的对象,生成异常对象的过程,声明在方法体内
    • throws:属于异常处理的一种方式,声明在方法的声明处
典型例题
class Student{
	
	private int id;
	
	public void regist(int id)  {
		if (id > 0) {
			this.id = id;
		}else {
			// System.out.println("您输入的数据非法");
			// 手动抛出异常对象
			// throw new RuntimeException("输入的数据非法");
			// throw new Exception("输入的数据非法");
			throw new MyException("不能输入负数");
		}
	}

	@Override
	public String toString() {
		return "Student [id=" + id + "]";
	}
}

自定义异常类

  • 如何自定义一个异常类
    • 继承于现有的异常结构:RuntimeException Exception
    • 提供全局常量 serialVersionUID
    • 提供重载的构造器
public class MyException extends RuntimeException{

	static final long serialVersionUID = 12345678921234L;

	public MyException() {
	}
	
	public MyException(String msg) {
		super(msg);
	}
}

多线程

程序、进程和线程的理解

程序(program)
概念

是为完成特定任务、用某种语言编写的一组指令的集合。即指一 段静态的代码

进程(process)
概念

是程序的一次执行过程,或是正在运行的一个程序

说明

进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域

线程(thread)
概念

进程可进一步细化为线程,是一个程序内部的一条执行路径。

说明

线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小

image-20211120134047178

内存结构

image-20211104104803420

进程可以细化为多个线程

每个线程拥有自己独立的:栈、程序计数器

多个线程共享同一个进程中的结构:方法区、堆

并行与并发

单核 CPU 与 多核 CPU 的理解
  • 单核 CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程 的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费 才能通过,那么 CPU 就好比收费人员。如果有某个人不想交钱,那么收费人员可以 把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。但是因为 CPU 时间单元特别短,因此感觉不出来。

  • 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)

  • 一个Java应用程序java.exe,其实至少有三个线程:main() 主线程,gc() 垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

并行与并发的理解
  • **并行:**多个CPU同时执行多个任务。比如:多个人同时做不同的事。
  • **并发:**一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。

创建多线程的两种方式

方式一:继承 Thread 类的方式
  1. 创建一个继承于 Thread 类的子类

  2. 重写 Thread 类的 run() --> 将此线程声明在 run() 中

  3. 创建继承于 Thread 类的子类的对象

  4. 通过此对象调用 start():

    1. 启动当前线程
    2. 调用当前线程的 run()

说明两个问题

  • 启动一个线程,必须调用 start(),不能调用 run() 的方式启动线程
  • 如果再启动一个线程,必须重新创建一个 Thread 子类的对象,调用此对象的 start()
方式二:实现 Runnable 接口的方式
  1. 创建一个实现了 Runnable 接口的类

  2. 实现类去实现 Runnable 中的抽象方法:run()

  3. 创建实现类的对象

  4. 将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象

  5. 通过 Thread 类的对象调用 start()

两种方式的对比
  • 开发中,优先选择实现 Runnable 接口的方式

    原因:

    • 实现的方式没有类的单继承性的局限性
    • 实现的方式更适合来处理多个线程有共享数据的情况
  • 联系:public class Tread implements Runnable
  • 相同点:
    • 两种方式都需要重写 run(),将线程要执行的逻辑声明在 run() 中
    • 目前两种方式,要想启动线程,都是调用的 Thread 类中的 run()

Thread 类中的常用方法

常用方法
  1. start():启动当前线程,调用当前线程的 run()

  2. run():通常需要重写 Thread 类中的此方法,将创建的线程要执行的操作声明在此方法中

  3. currentThread():静态方法,返回执行当前代码的线程

  4. getName():获取当前线程的名字

  5. setName():设置当前线程的名字

  6. yeild():释放当前 CPU 的执行权

  7. join():在线程 a 中调用线程 b 的 join(),此时线程 a 就进入阻塞状态,直到线程 b 完全执行完以后,线程 a 才结束阻塞状态

  8. stop():已过时,当执行此方法时,强制结束当前线程

  9. sleep(long millis):让当前线程“睡眠”指定的 millis 毫秒,在指定的 millis 毫秒时间内,当前线程是阻塞状态

  10. isAlive():判断当前线程是否存活

线程的优先级
  1. MAX_PRIORITY:10

    MIN _PRIORITY:1

    NORM_PRIORITY:5 --> 默认优先级

  2. 如何获取和设置当前线程的优先级

    • getPriority():获取线程的优先级

    • setPriority(int newPriority):设置线程的优先级

    • 说明:高优先级的线程要抢占低优先级线程 CPU 的执行权,但是只是从概率上讲,高优先级的线程高概率的情况下被执行,并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行

线程的通信:wait()、notify()、 notifyAll():此三个方法定义在 Object 类中的

线程的分类
  • 守护线程
  • 用户线程

Thread 的生命周期

image-20211124142511266

说明

  • 生命周期关注两个概念:状态,相应的方法

  • 关注

    • 状态 a --> 状态 b:哪些方法执行了(回调方法)
    • 某个方法主动调用:状态 a --> 状态 b
  • 阻塞:临时状态,不可以作为最终状态

    死亡:最终状态

线程的同步机制

背景

创建三个窗口买票,总票数为100张,使用实现 Runnable 接口的方式

  1. 问题,买票过程中,出现了重票、错票 --> 出现了线程安全的问题

  2. 出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其它线程参与进来,也操作车票

  3. 如何解决:当一个线程 a 在操作 ticket 的时候,其它线程不能参与进来,直到线程 a 操作完 ticket 时,其它线程才可以开始操作 ticket,这种情况即使线程 a 出现了阻塞,也不能改变

Java 解决方案:同步机制

在 Java 中,通过同步机制,来解决线程的安全问题

  • 方式一:同步代码块

    synchronized(同步监视器){
    	// 需要被同步的代码
    }
    
    • 说明:

      • 操作共享数据的代码,即为需要被同步的代码 --> 不能包含代码多了,也不能包含代码少了
      • 共享数据:多个线程共同操作的变量,比如:ticket 就是共享数据

      • 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁

        要求:多个线程必须公用同一把锁

    • **补充 **

      • 在实现 Runnable 接口创建多线程的方式中,可以考虑 this 充当同步监视器

      • 继承 Thread 类创建多线程的方式中,慎用 this 充当同步监视器,考虑使用当前类充当同步监视器

  • 方式二:同步方法

    • 如果操作共享数据的代码完整的声明在一个方法中,不妨将此方法声明为同步的

    • 同步方法仍然涉及到同步监视器,只是不需要显示声明

    • 非静态同步方法的同步监视器是:this

      静态同步方法的同步监视器是:当前类本身

  • 方式三:Lock 锁 — JDK 5.0新增

    • 面试题:synchronized 与 Lock 的异同
      • 同:二者都可以解决线程安全问题
      • 异:
        • synchronized 机制在执行完相应的同步代码以后,自动的释放同步监视器
        • Lock 需要手动的启动同步(lock()),同时,结束同步也要手动的实现(unLock())

优先使用顺序:Lock –> 同步代码块(已经进入了方法体,分配了相应资源) –> 同步方法(在方法体之外)

利弊
  • 好处:同步的方式,解决了线程的安全问题
  • 局限性:操作同步代码时,只能有一个线程参与,其它线程等待,相当于是一个单线程的过程,效率低
面试题
  • Java 是如何解决线程安全问题的,有几种方式?并对比几种方式的不同?
  • synchronized 和 Lock 方式解决线程安全问题的对比
线程安全的单例模式(懒汉式)
class Bank{

    private static Bank instance = null;


    private Bank(){
    }

    public static Bank getInstance() {
        // 方式一:效率稍差
        /*synchronized (Bank.class) {
            if (instance == null){
                instance = new Bank();
            }
            return instance;
        }*/
        // 方式二:效率更高
        if (instance == null){
            synchronized (Bank.class) {
                if (instance == null){
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

面试题:写一个线程安全的单例模式

  • 饿汉式
  • 懒汉式
死锁问题
死锁的理解

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁

说明
  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
  • 使用同步时,要避免出现死锁
public class ThreadTest {
    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread(){
            @Override
            public void run() {
                synchronized (s1){
                    s1.append("a");
                    s2.append("1");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");

                        System.out.println("s1 = " + s1);
                        System.out.println("s2 = " + s2);
                    }
                }
            }
        }.start();

        new Thread(() -> {
            synchronized (s2){
                s1.append("c");
                s2.append("3");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (s1){
                    s1.append("d");
                    s2.append("4");

                    System.out.println("s1 = " + s1);
                    System.out.println("s2 = " + s2);
                }
            }
        }).start();
    }
}

线程通信

涉及到的三个方法
  • wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
  • notify():一旦执行此方法,就会唤醒被 wait 的一个线程,如果有多个线程被 wait,就唤醒优先级高的那个
  • notifyAll():一旦执行此方法,就会唤醒所有被 wait 的线程
说明
  • wait() notify() notifyAll() 三个方法必须使用在同步代码块或同步方法中
  • wait() notify() notifyAll() 三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会出现 IllegalMonitorStateException 异常
  • wait() notify() notifyAll() 三个方法是定义在 java.lang.Object 类中
面试题

sleep() 和 wait() 的异同

  • 相同点:

    • 一旦执行方法,都可以使得当前线程进入阻塞状态
  • 不同点:

    • 两个方法声明的位置不同:Thread 类中声明 sleep(),Object 类中声明 wait()

    • 调用的要求不同:sleep() 可以在任何需要的场景下调用,wait() 必须使用在同步代码块或同步方法中

    • 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep() 不会释放锁,wait() 会释放锁

小结
  • 会释放锁的操作
    • 当前线程的同步方法、同步代码块执行结束。
    • 当前线程在同步代码块、同步方法中遇到 break、return 终止了该代码块、该方法的继续执行。
    • 当前线程在同步代码块、同步方法中出现了未处理的 Error 或 Exception,导致异常结束。
    • 当前线程在同步代码块、同步方法中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁。
  • 不会释放锁的操作
    • 线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行
    • 线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁(同步监视器)。
      • 应尽量避免使用 suspend() 和 resume() 来控制线程

JDK 5.0新增线程创建的方式

方式一

创建线程的方式三:实现 Callable 接口 — JDK 5.0新增

步骤

  1. 创建一个实现实现 Callable 的实现类
  2. 实现 call 方法,将此线程需要执行的操作声明在 call() 中
  3. 创建 Callable 接口的实现类
  4. 将此 Callable 接口实现类的对象作为传递到 FutureTask 构造器中,创建 FutureTask 的实现类
  5. 将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start()
  6. 获取 Callable 中 call() 方法的返回值
public class ThreadNew {
    public static void main(String[] args) {
        NumTread numTread = new NumTread();
        FutureTask future = new FutureTask(numTread);
        new Thread(future).start();
        try {
            // get() 返回值即为 FutureTask 构造器形参 Callable 实现类重写的 call() 的返回值
            Object sum = future.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class NumTread implements Callable{
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

说明

  • 如何理解实现 Callable 接口创建多线程方式比实现 Runnable 接口创建多线程方式强大
    • call() 可以有返回值
    • call() 可以抛出异常,被外面的操作捕获,获取异常信息
    • Callable 是支持泛型的
方式二

创建线程的方式四:使用线程池

步骤

  1. 提供指定线程数量的线程池
  2. 执行指定的线程的操作,需要提供实现 Runnable 接口或 Callable 接口实现类的对象
  3. 关闭连接池
public class ThreadPool {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor executor = (ThreadPoolExecutor) service;
        // 设置线程池属性
        // System.out.println(service.getClass());
        // executor.setCorePoolSize(15);
        // executor.setKeepAliveTime();

        // 适合使用于 Runnable
        service.execute(new NumberThread());
        service.execute(new NumberThread1());
        // service.submit(); 适合使用于 Callable

        service.shutdown();
    }
}

class NumberThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

class NumberThread1 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if (i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

好处

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止

面试题:Java 中多线程的创建有几种方式?四种

常用类

String 类的使用

概述

String:字符串,使用一对""引起来表示

  1. String 声明为 final 的,不可被继承

  2. String 实现了 Serializable 接口:表示字符串是支持序列化的

    String 实现了 Comparable 接口:表示 String 可以比较大小

  3. String 内部定义了 final char[] value 用于存储字符串数据

  4. 通过字面量的方式给一个字符串赋值,此时的字符串值声明在字符串常量池中

  5. 字符串常量池中是不会存储相同内容(使用 String 类的 equals() 比较,返回 true)的字符串的

String的不可变性
说明
  • 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的 value 进行赋值
    
  • 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的 value 进行赋值
    
  • 当调用 String 的 replace() 方法修改指定的字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的 value 进行赋值
    
代码举例
@Test
public void test1(){
    // 字面量的定义方式
    String s1 = "abc";
    String s2 = "abc";
    s1 = "hello";
    // 比较 s1 和 s2 的地址值
    System.out.println("s1 == s2 = " + (s1 == s2));
    System.out.println("s1 = " + s1);
    System.out.println("s2 = " + s2);

    String s3 = "abc";
    s3 += "def";
    System.out.println("s3 = " + s3);
    System.out.println("s2 = " + s2);

    String s4 = "abc";
    String s5 = s4.replace('a', 'm');
    System.out.println("s4 = " + s4);
    System.out.println("s5 = " + s5);
}
图示

image-20211125140441131

String 实例化的不同方式
方式说明
  • 方式一:通过字面量定义的方式
  • 方式二:通过构造器的方式
代码举例
@Test
public void test2(){
    // 声明在方法区中的字符串常量池中
    String s1 = "JavaEE";
    String s2 = "JavaEE";

    // 保存的地址值是数据在堆空间中开辟以后对应的地址值
    String s3 = new String("JavaEE");
    String s4 = new String("JavaEE");
    System.out.println(s1 == s2); // true
    System.out.println(s1 == s3); // false
    System.out.println(s1 == s4); // false
    System.out.println(s3 == s4); // false
}
面试题
  • String s = new String(“abc”); 方式创建对象,在内存中创建了几个对象

    两个:一个是堆空间中 new 结构,另一个是 char[] 对应的常量池中的数据:“abc”

图示

image-20211125141028203

字符串拼接方式赋值的对比
说明
  • 常量与常量的拼接结果在常量池。且常量池中不会存在相同内容的常量。
  • 只要其中有一个是变量,结果就在堆中
  • 如果拼接的结果调用 intern() 方法,返回值就在常量池中
代码举例
@Test
public void test3(){
    String s1 = "javaEE";
    String s2 = "hadoop";
    String s3 = "javaEEhadoop";
    String s4 = "javaEE" + "hadoop";
    String s5 = s1 + "hadoop";
    String s6 = "javaEE" + s2;
    String s7 = s1 + s2;
    System.out.println(s3 == s4); // true
    System.out.println(s3 == s5); // false
    System.out.println(s3 == s6); // false
    System.out.println(s3 == s7); // false
    System.out.println(s5 == s6); // false
    System.out.println(s5 == s7); // false
    System.out.println(s6 == s7); // false

    // 返回值得到的 s8 使用的常量值中已经存在的"javaEEhadoop"
    String s8 = s5.intern();
    System.out.println(s8 == s4); // true
}
@Test
public void test4(){
    String s1 = "javaEEhadoop";
    String s2 = "javaEE";
    String s3 = s2 + "hadoop";
    System.out.println(s1 == s3);

    final String s4 = "javaEE";
    String s5 = s4 + "hadoop";
    System.out.println(s1 == s5);

}
常用方法
  1. int length():返回字符串的长度: return value.length

  2. char charAt(int index): 返回某索引处的字符 return value[index]

  3. boolean isEmpty():判断是否是空字符串:return value.length == 0

  4. String toLowerCase():使用默认语言环境,将 String 中的所有字符转换为小写

  5. String toUpperCase():使用默认语言环境,将 String 中的所有字符转换为大写

  6. String trim():返回字符串的副本,忽略前导空白和尾部空白

  7. boolean equals(Object obj):比较字符串的内容是否相同

  8. boolean equalsIgnoreCase(String anotherString):与 equals 方法类似,忽略大小写

  9. String concat(String str):将指定字符串连接到此字符串的结尾。 等价于用“+”

  10. int compareTo(String anotherString):比较两个字符串的大小

  11. String substring(int beginIndex): 返回一个新的字符串, 它是此字符串的从 beginIndex 开始截取到最后的一个子字符串。

  12. String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从 beginIndex 开始截取到 endIndex (不包含)的一个子字符串。

  13. boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束

  14. boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始

  15. boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始

  16. boolean contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列时,返回 true

  17. int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引

  18. int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始

  19. int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引

  20. int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索

    indexOf 和 lastIndexOf 方法如果未找到都是返回-1

  21. String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。

  22. String replace(CharSequence target, CharSequence replacement):使 用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。

  23. String replaceAll(String regex, String replacement) :使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。

  24. String replaceFirst(String regex, String replacement) :使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。

  25. boolean matches(String regex):告知此字符串是否匹配给定的正则表达式。

  26. String[] split(String regex):根据给定正则表达式的匹配拆分此字符串。

  27. String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此字符串,最多不超过 limit 个,如果超过了,剩下的全部都放到最后一个元素中。

String 与其它结构的转换
与基本数据类型、包装类之间的转换
  • String --> 基本数据类型、包装类:调用包装类的静态方法:parseXxx(str)
  • 基本数据类型、包装类 --> String:调用 String 重载的 valueOf(xxx)
@Test
public void test1(){
    String str1 = "123";
    int num = Integer.parseInt(str1);
    String str2 = String.valueOf(num);
    String str3 = num + "";

    System.out.println(str1 == str3);
}
与 char[] 之间的转换
  • String --> char[]:调用 String 的 toCharArray()
  • char[] --> String:调用 String 的 构造器
@Test
public void test2(){
    String str1 = "abc123";
    char[] charArray = str1.toCharArray();
    for (int i = 0; i < charArray.length; i++) {
        System.out.println(charArray[i]);
    }

    char[] arr = new char[]{'h', 'e', 'l', 'l', 'o'};
    String str2 = new String(arr);
    System.out.println(str2);
}
与 byte[] 之间的转换
  • 编码:String --> byte[]:调用 String 的 getBytes()
  • 解码:byte[] --> String:调用 String 的构造器

编码:字符串 --> 字节

解码:编码的逆过程 字节 --> 字符串

说明:解码时,要求解码使用的字符集必须与编码时使用的字符集一致,否则会出现乱码

@Test
public void test3() throws UnsupportedEncodingException {
    String str1 = "abc123中国";
    // 使用默认的字符集进行转换
    byte[] bytes = str1.getBytes();
    System.out.println(Arrays.toString(bytes));
    // 使用 gbk 进行编码
    byte[] gbks = str1.getBytes("gbk");
    System.out.println(Arrays.toString(gbks));
    String str2 = new String(bytes);
    System.out.println(str2);

    String str3 = new String(gbks, "gbk");
    System.out.println(str3);
}
与 StringBuffer、StringBuilder 之间的转换
  • String –> StringBuffer、StringBuilder:调用 StringBuffer、StringBuilder 的构造器
  • StringBuffer、StringBuilder -> String:
    • 调用 String 的构造器
    • StringBuffer、StringBuilder 的 toString
JVM 中字符串常量池存放位置说明
  • JDK6.0:字符串常量池存储在方法区(永久区)
  • JDK7.0:字符串常量池存储在堆空间
  • JDK8.0:字符串常量池存储在方法区(元空间)
成绩算法题目的考查
  1. 模拟一个trim方法,去除字符串两端的空格。
public String myTrim(String str) {
    if (str != null) {
        // 用于记录从前往后首次索引位置不是空格的位置的索引
        int start = 0;
        // 用于记录从后往前首次索引位置不是空格的位置的索引
        int end = str.length() - 1;

        while (start < end && str.charAt(start) == ' ') {
            start++;
        }

        while (start < end && str.charAt(end) == ' ') {
            end--;
        }
        if (str.charAt(start) == ' ') {
            return "";
        }

        return str.substring(start, end + 1);
    }
    return null;
}
  1. 将一个字符串进行反转。将字符串中指定部分进行反转。比如“abcdefg”反转为”abfedcg”
/**
 * 方式一:转换为 char[]
 * @param str
 * @param startIndex
 * @param endIndex
 * @return
 */
public String reverseChar(String str, int startIndex, int endIndex){
    if (str != null) {
        char[] arr = str.toCharArray();
        for(int x = startIndex, y = endIndex; x < y; x++, y--){
            char temp = arr[x];
            arr[x] = arr[y];
            arr[y] = temp;
        }
        return new String(arr);
    }
    return null;
}

/**
 * 方式二:使用 String 的拼接
 * @param str
 * @param startIndex
 * @param endIndex
 * @return
 */
public String reverseString(String str, int startIndex, int endIndex){
    if (str != null) {
        String reverseStr = str.substring(0, startIndex);
        for(int i = endIndex; i >= startIndex; i--){
            reverseStr += str.charAt(i);
        }
        reverseStr += str.substring(endIndex + 1);
        return reverseStr;
    }
    return null;
}

/**
 * 方式三:使用 StringBuffer / StringBuilder 替换 String
 * @param str
 * @param startIndex
 * @param endIndex
 * @return
 */
public String reverseStringBuilder(String str, int startIndex, int endIndex){
    if (str != null) {
        StringBuilder builder = new StringBuilder(str.length());
        builder.append(str.substring(0, startIndex));
        for(int i = endIndex; i >= startIndex; i--){
            builder.append(str.charAt(i));
        }
        builder.append(str.substring(endIndex + 1));
    }
    return null;
}
  1. 获取一个字符串在另一个字符串中出现的次数。比如:获取“ab”在“abkkcadkabkebfkabkskab” 中出现的次数
/**
 * 获取 subStr 在 mainStr 中出现的次数
 * @param mainStr
 * @param subStr
 * @return
 */
public int getCount(String mainStr, String subStr){
    int mainLength = mainStr.length();
    int subLength = subStr.length();
    int count = 0;
    int index = 0;
    if (mainLength >= subLength){
        // 方式一
        /*while ((index = mainStr.indexOf(subStr)) != -1){
            count++;
            mainStr = mainStr.substring(index + subStr.length());
        }*/
        // 方式二
        while ((index = mainStr.indexOf(subStr, index)) != -1){
            count++;
            index += subLength;
        }
    }
    return count;
}
  1. 获取两个字符串中最大相同子串。比如 str1 = "abcwerthelloyuiodef“;str2 = “cvhellobnm”

    提示:将短的那个串进行长度依次递减的子串与较长的串比较。

/**
 * 如果存在多个长度相同的最大相同子串
 * 此时先返回String[],后面可以用集合中的ArrayList替换,较方便
 * @param str1
 * @param str2
 * @return
 */
public String[] getMaxSameSubString(String str1, String str2) {
    if (str1 != null && str2 != null) {
        StringBuffer sBuffer = new StringBuffer();
        String maxString = (str1.length() > str2.length()) ? str1 : str2;
        String minString = (str1.length() > str2.length()) ? str2 : str1;

        int len = minString.length();
        for (int i = 0; i < len; i++) {
            for (int x = 0, y = len - i; y <= len; x++, y++) {
                String subString = minString.substring(x, y);
                if (maxString.contains(subString)) {
                    sBuffer.append(subString + ",");
                }
            }
            // System.out.println(sBuffer);
            if (sBuffer.length() != 0) {
                break;
            }
        }
        String[] split = sBuffer.toString().replaceAll(",$", "").split("\\,");
        return split;
    }
    return null;
}
  1. 对字符串中字符进行自然顺序排序。

    提示:

    1. 字符串变成字符数组。
    2. 对数组排序,选择,冒泡,Arrays.sort();
    3. 将排序后的数组变成字符串。

StringBuffer、StringBuilder

String、StringBuffer、StringBuilder 三者的比较
  • String:不可变的字符序列;底层使用 char[] 存储
  • StringBuffer:可变的字符序列;线程安全的,效率偏低;底层使用 char[] 存储
  • StringBuilder:可变的字符序列;JDK 5.0新增线程不安全,效率高;底层使用 char[] 存储
StringBuffer 与 StringBuilder 的内存解析

以 StringBuffer 为例

String str = new String(); // char[] value = new char[0];
String str = new String("abc"); // char[] value = new char[]{'a', 'b', 'c'};
StringBuffer sb1 = new StringBuffer(); // char[] value = new char[16];底层创建了一个长度是16的数组
sb1.append('a') // value[0] = 'a';
sb1.append('b') // value[1] = 'b';
StringBuffer sb2 = new StringBuffer("abc"); // char[] value = new char["abc".length() + 16];
  • 问题1:System.out.println(sb2.length()); // 3

  • 问题2:扩容问题:如果要添加的数据底层数组撑不下了,那就需要扩容底层的数组,默认情况下,扩容为原来容量的2倍 + 2,同时将原有数组中的元素复制到新的数组中

    指导意义:开发中建议使用:StringBuffer(int capacity)StringBuilder(int capacity)

对比String、StringBuffer、StringBuilder 三者的执行效率
  • 从高到低:StringBuilder > StringBuffer > String
StringBuffer、StringBuilder 中的常用方法
  • StringBuffer append(xxx):提供了很多的append()方法,用于进行字符串拼接
  • StringBuffer delete(int start,int end):删除指定位置的内容
  • StringBuffer replace(int start, int end, String str):把[start,end)位置替换为str
  • StringBuffer insert(int offset, xxx):在指定位置插入 xxx
  • StringBuffer reverse():把当前字符序列逆转
  • int indexOf(String str)
  • String substring(int start,int end):返回一个 从 start 开始到 end 索引结束的左闭右开区间的子字符串
  • int length()
  • char charAt(int n)
  • void setCharAt(int n ,char ch)

总结:

  • 增:append(xxx)
  • 删:delete(int start, int end)
  • 改:setCharAt(int n ,char ch) / replace(int start, int end, String str)
  • 查:charAt(int n)
  • 插:insert(int offset, xxx)
  • 长度:length()
  • 遍历:for + charAt() / toString()

JDK8 之前的日期时间 API

获取系统当前时间

System 类中的 currentTimeMillis():返回当前时间与1970年1月1日0时0分0秒之间以毫秒为单位的时间差,称为时间戳

java.util.Date 类和 java.sql.Date 类

image-20211125151436098

  • 两个构造器的使用
    • 构造器一:Date():创建一个对应当前时间的 Date 对象
    • 构造器二:Date(long date):创建指定毫秒数的 Date 对象
  • 两个方法的使用
    • toString():显示当前的年月日时分秒
    • getTime():获取当前 Date 对象对应的毫秒数(时间戳)
  • java.sql.Date 对应着数据库自动日期类型的变量
    • 如何实例化
    • 如何将 java.util.Date 对象转换成 java.sql.Date 对象
@Test
public void test2(){
    // 构造器一:Date():创建一个对应当前时间的 Date 对象
    Date date1 = new Date();
    System.out.println(date1);
    System.out.println(date1.getTime());
    // 构造器二:Date(long date):创建指定毫秒数的 Date 对象
    Date date2 = new Date(1637809383273L);
    System.out.println(date2);

    java.sql.Date date3 = new java.sql.Date(99999323820232L);
    System.out.println(date3);
    Date date4 = new Date(221223445L);
    // java.sql.Date date5 = (java.sql.Date) date4;
    java.sql.Date date5 = new java.sql.Date(date3.getTime());
}
java.text.SimpleDateFormat 类

SimpleDateFormat 对日期 Date 类的格式化和解析

  1. 两个操作
    1. 格式化:日期 --> 字符串
    2. 解析:格式化的逆过程 字符串 --> 日期
  2. SimpleDateFormat 的实例化:构造器
// 按照指定的方式格式化和解析:调用带参数的构造器
// SimpleDateFormat sdf1 = new SimpleDateFormat("yyyyy.MMMMM.dd GGG hh:mm aaa");
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
// 格式化
System.out.println(sdf1.format(date));
// 解析:要求字符串必须符合 SimpleDateFormat 识别的格式(通过构造器参数体现),否则抛异常
System.out.println(sdf1.parse(sdf1.format(date)));

练习

/**
 * 练习一:字符串“2020-09-08”转换为 java.sql.Date
 * 练习二:三天打鱼两天晒网 1990-01-01 xxxx-xx-xx 打鱼 晒网
 * 总天数 % 5 == 1, 2, 3 : 打鱼
 * 总天数 % 5 == 4, 0    : 晒网
 * 总天数的计算
 * 方式一:(date2.getTime() - date1.getTime()) / (1000 * 60 * 60 * 24)
 * 方式二:1990-01-01  --> 2019-12-31 + 2020-01-01 --> 2020-09-08
 */
@Test
public void testExer() throws ParseException {
    String birth = "2020-09-08";
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    Date date = simpleDateFormat.parse(birth);
    java.sql.Date birthDate = new java.sql.Date(date.getTime());
    System.out.println(birthDate);
}
Calendar 类:日历类、抽象类
@Test
public void testCalendar(){
    // 1.实例化
    // 方式一:创建其子类(GregorianCalendar)的对象
    // 方式二:调用其静态方法 getInstance()
    Calendar calendar = Calendar.getInstance();
    // System.out.println(calendar.getClass());
    // 2.常用方法
    // get()
    int days = calendar.get(Calendar.DAY_OF_MONTH);
    System.out.println(days);
    System.out.println(calendar.get(Calendar.DAY_OF_YEAR));
    // set()
    // Calender 可变性
    calendar.set(Calendar.DAY_OF_MONTH, 22);
    System.out.println(calendar.get(Calendar.DAY_OF_MONTH));
    // add()
    calendar.add(Calendar.DAY_OF_MONTH, -3);
    System.out.println(calendar.get(Calendar.DAY_OF_MONTH));
    // getTime():日历类 --> Date
    Date date = calendar.getTime();
    System.out.println(date);
    // setTime():Date --> 日历类
    Date date1 = new Date();
    calendar.setTime(date1);
    System.out.println(calendar.get(Calendar.DAY_OF_MONTH));
}

JDK8 中新的日期时间 API

日期时间 API 的迭代

第一代:JDK1.0 Date 类

第二代:JDK1.1 Calendar 类,一定程度上替代了 Date 类

第三代:JDK1.8 提出了一套新的 API

前两代存在的问题举例
  • 可变性:像日期和时间这样的类应该是不可变的。

  • 偏移性:Date中的年份是从1900开始的,而月份都从0开始。

  • 格式化:格式化只对Date有用,Calendar则不行。

  • 此外,它们也不是线程安全的;不能处理闰秒等。

JDK8 中新的日期时间 API涉及到的包
  • java.time:包含值对象的基础包
  • java.time.chrono:提供对不同的日历系统的访问
  • java.time.format:格式化和解析时间和日期
  • java.time.temporal:包括底层框架和扩展特性
  • java.time.zone:包含时区支持的类

说明:大多数开发者只会用到基础包和format包,也可能会用到temporal包。因此,尽管有68个新的公开类型,大多数开发者,大概将只会用到其中的三分之一。

LocalDate、LocalTime、LocalDateTime
说明
  1. 分别表示使用 ISO-8601日历系统的日期、时间、日期和时间。 它们提供了简单的本地日期或时间,并不包含当前的时间信息,也不包含与时区 相关的信息。
  2. LocalDateTime 相较于 LocalDate、LocalTime,使用频率要高
  3. 类似于 java.util.Date 类
常用方法
方法描述
now() / * now(ZoneId zone)静态方法,根据当前时间创建对象/指定时区的对象
of()静态方法,根据指定日期/时间创建对象
getDayOfMonth() / getDayOfYear()获得月份天数(1-31) /获得年份天数(1-366)
getDayOfWeek()获得星期几(返回一个 DayOfWeek 枚举值)
getMonth()获得月份, 返回一个 Month 枚举值
getMonthValue() / getYear()获得月份(1-12) /获得年份
getHour() / getMinute() / getSecond()获得当前对象对应的小时、分钟、秒
withDayOfMonth() / withDayOfYear() / withMonth() / withYear()将月份天数、年份天数、月份、年份修改为指定的值并返回新的对象
plusDays() / plusWeeks() / plusMonth() / plusYears() / plusHours()向当前对象添加几天、几周、几个月、几年、几小时
minusMonths() / minusWeeks() / minusDays() / minusYears() / minusHours()从当前对象减去几月、几周、几天、几年、几小时
时间点:Instant
说明
  1. 时间线上的一个瞬时点,概念上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的毫秒数
  2. 类似于 java.util.Date 类
常用方法
方法描述
now()静态方法,返回默认UTC时区的Instant类的对象
ofEpochMilli(long epochMilli)静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒 数之后的Instant类的对象
atOfSet(ZoneOfSet ofSet)结合即时的偏移来创建一个 OffsetDateTime
toEpochMilli()返回1970-01-01 00:00:00到当前时间的毫秒数,即为时间戳

时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。

DateTimeFormatter
说明
  1. 格式化或解析日期,时间
  2. 类似于 SimpleDateFormat
常用方法
  • 实例化方式

    • 预定义的标准格式。
      • ISO_LOCAL_DATE_TIME
      • ISO_LOCAL_DATE
      • ISO_LOCAL_TIME
    • 本地化相关的格式。如:ofLocalizedDateTime(FormatStyle.LONG)
    • 自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)
  • 常用方法

    方法描述
    ofPattern(String pattern)静态方法,返回一个指定字符串格式的 DateTimeFormatter
    format(TemporalAccessor t)格式化一个日期、时间,返回字符串
    parse(CharSequence text)将指定格式的字符序列解析为一个日期、时间

特别的:自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)

// 方式三:自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)
DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println(formatter3.format(localDateTime));

TemporalAccessor accessor = formatter3.parse("2021-11-25 21:53:44");
System.out.println(accessor);
其它 API 的使用
带时区的日期时间:ZonedDateTime / ZoneId
/**
 * ZoneId:类中包含了所有时区信息
 */
@Test
public void test4(){
    // getAvailableZoneIds():获取对应的 ZoneId
    Set<String> zoneIds = ZoneId.getAvailableZoneIds();
    for (String zoneId : zoneIds) {
        System.out.println(zoneId);
    }
    System.out.println();

    // 获取"Asia/Tokyo"时区对应的时间
    System.out.println(LocalDateTime.now(ZoneId.of("Asia/Tokyo")));
}

/**
 * ZonedDateTime:带时区的日期时间
 */
@Test
public void test5(){
    // now():获取本时区的 ZonedDateTime 对象
    System.out.println(ZonedDateTime.now());
    // now(ZoneId id):获取指定时区的 ZonedDateTime 对象
    ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
}
时间间隔:Duration

用于计算两个“时间”间隔,以秒和纳秒为基准

方法描述
between(Temporal start, Temporal end)静态方法,返回 Duration 对象,表示两个时间的间隔
getNano() / getSeconds()返回时间间隔的纳秒数 / 返回时间间隔的秒数
toDays() / toHours() / toMinutes() / toMillis() / toNano()返回时间间隔的天数、小时数、分钟数、毫秒数、纳秒数
@Test
public void test6(){
    //Duration:用于计算两个“时间”间隔,以秒和纳秒为基准  
    LocalTime localTime = LocalTime.now();
    LocalTime localTime1 = LocalTime.of(15, 23, 32);
    //between():静态方法,返回Duration对象,表示两个时间的间隔  
    Duration duration = Duration.between(localTime1, localTime);  		
    System.out.println(duration);

    System.out.println(duration.getSeconds()); 
    System.out.println(duration.getNano());

    LocalDateTime localDateTime = LocalDateTime.of(2016, 6, 12, 15, 23, 32);
    LocalDateTime localDateTime1 = LocalDateTime.of(2017, 6, 12, 15, 23, 32);

    Duration duration1 = Duration.between(localDateTime1, localDateTime);  
    System.out.println(duration1.toDays());
}
时间间隔:Period

用于计算两个“日期”间隔,以年、月、日衡量

方法描述
between(LocalDate start, LocalDate end)静态方法,返回 Period 对象,表示两个本地日期的间隔
getYears() / getMonths() / getDays()返回此期间的年数,月数、天数
withYears(int years) / withMonths(int months) / withDays(int days)返回设置间隔指定年、月、日数以后的 Period 对象
@Test
public void test7(){
    LocalDate localDate = LocalDate.now();
    LocalDate localDate1 = LocalDate.of(2028, 3, 18);

    Period period = Period.between(localDate, localDate1);  
    System.out.println(period);

    System.out.println(period.getYears()); 
    System.out.println(period.getMonths());
    System.out.println(period.getDays());

    Period period1 = period.withYears(2);  
    System.out.println(period1);

}
时间校正器:TemporalAdjuster
@Test
public void test8(){
    // 获取当前日期的下一个周日是哪天?
    TemporalAdjuster temporalAdjuster = TemporalAdjusters.next(DayOfWeek.SUNDAY);  
    LocalDateTime localDateTime = LocalDateTime.now().with(temporalAdjuster);  
    System.out.println(localDateTime);
    // 获取下一个工作日是哪天?
    LocalDate localDate = LocalDate.now().with(new TemporalAdjuster() {
        @Override
        public Temporal adjustInto(Temporal temporal) {
            LocalDate date = (LocalDate) temporal;
            if (date.getDayOfWeek().equals(DayOfWeek.FRIDAY)) {
                return date.plusDays(3);
            } else if (date.getDayOfWeek().equals(DayOfWeek.SATURDAY)) {  
                return date.plusDays(2);
            } else {
                return date.plusDays(1);
            }
        }
    });
    System.out.println("下一个工作日是:" + localDate);
}

Java比较器

使用背景

Java 中的对象,正常情况下,只能进行比较:== 或 != ,不能使用 > 或 < 的,但是在开发场景中,需要对多个对象进行排序,言外之意,就需要比较对象的大小,如何实现?使用两个接口中的任何一个:Comparable 或 Comparator

自然排序:使用 Comparable 接口
说明
  1. 像 String、包装类等实现了 Comparable 接口,重写了 compareTo(obj) 方法,给出了比较两个对象大小的方式
  2. 像 String、包装类等重写了 compareTo(obj) 方法以后,进行了从小到大的规则
  3. 重写 compareTo(obj) 的规则:
    1. 如果当前对象 this 大于形参对象 obj,则返回正整数
    2. 如果当前对象 this 小于形参对象 obj,则返回负整数
    3. 如果当前对象 this 等于形参对象 obj,则返回零
  4. 对于自定义类来说,如果需要排序,可以自定义类实现 Comparable 接口,重写 compareTo(obj) 方法,在 compareTo(obj) 方法中指明如何排序
自定义类代码举例
public class Goods implements Comparable{

    private String name;

    private double price;

    public Goods() {
    }

    public Goods(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

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

    /**
     * 指明商品比较大小的方式:按照价格从低到高排序,再按照产品名称从低到高排序
     * @param o
     * @return
     */
    @Override
    public int compareTo(Object o) {
        if (o instanceof Goods){
            Goods goods = (Goods) o;
            // 方式一
            if (this.price > goods.price){
                return 1;
            }else if (this.price < goods.price){
                return -1;
            }else {
                return this.name.compareTo(goods.name);
            }
            // 方式二
            // return Double.compare(this.price, goods.price);
        }
        throw new RuntimeException("传入的数据类型不一致");
    }
}

定制排序:使用 Comparator 接口
说明
  1. 背景

    当元素的类型没有实现 java.lang.Comparable 接口而又不方便修改代码,或者实现了java.lang.Comparable 接口的排序规则不适合当前的操作,那么可以考虑使用 Comparator 的对象来排序

  2. 重写 compare(Object o1,Object o2) 方法,比较 o1 和 o2 的大小:

    1. 如果返回正整数,表示 o1 大于 o2;
    2. 如果返回0,表示相等;
    3. 如果返回负整数,表示 o1 小于 o2
代码举例
Comparator comparator = new Comparator() {
    /**
     * 按照字符串从大到小排序
     * @param o1
     * @param o2
     * @return
     */
    @Override
    public int compare(Object o1, Object o2) {
        if (o1 instanceof String && o2 instanceof  String){
            String s1 = (String) o1;
            String s2 = (String) o2;
            return -s1.compareTo(s2);
        }
        throw new RuntimeException("输入的数据类型不一致");
    }
};
两种排序方式对比
  • Comparable 接口的方式一旦一定,保证 Comparable 接口实现类的对象在任何位置都可以比较大小
  • Comparator 接口属于临时性的比较

其它类

System 类
  • System类代表系统,系统级的很多属性和控制方法都放置在该类的内部。 该类位于java.lang包。

  • 由于该类的构造器是private的,所以无法创建该类的对象,也就是无法实 例化该类。其内部的成员变量和成员方法都是static的,所以也可以很方便 的进行调用。

  • 方法

    • native long currentTimeMillis():该方法的作用是返回当前的计算机时间,时间的表达格式为当前计算机时间和GMT时间(格林威治时间)1970年1月1号0时0分0秒所差的毫秒数。

    • void exit(int status): 该方法的作用是退出程序。其中status的值为0代表正常退出,非零代表 异常退出。使用该方法可以在图形界面编程中实现程序的退出功能等。

    • void gc():该方法的作用是请求系统进行垃圾回收。至于系统是否立刻回收,则 取决于系统中垃圾回收算法的实现以及系统执行时的情况。

    • String getProperty(String key):该方法的作用是获得系统中属性名为key的属性对应的值。系统中常见的属性名以及属性的作用如下表所示:

      属性名属性说明
      java.versionJava 运行时环境版本
      java.homeJava安装目录
      os.name操作系统的名称
      os.version操作系统的版本
      user.name用户的账户名称
      user.home用户的主目录
      user.dir用户的当前工作目录
Math 类

java.lang.Math 提供了一系列静态方法用于科学计算。其方法的参数和返回值类型一般为 double 型。

BigInteger 类、BigDecimal 类

说明

  • java.math 包的 BigInteger 可以表示不可变的任意精度的整数。
  • 要求数字精度比较高,故用到 java.math.BigDecimal 类

代码举例

@Test
public void testBigInteger() {
    BigInteger bi = new BigInteger("12433241123");
    BigDecimal bd = new BigDecimal("12435.351");
    BigDecimal bd2 = new BigDecimal("11");
    System.out.println(bi);
    // System.out.println(bd.divide(bd2));
    System.out.println(bd.divide(bd2, BigDecimal.ROUND_HALF_UP));
    System.out.println(bd.divide(bd2, 25, BigDecimal.ROUND_HALF_UP));
}

枚举类和注解

枚举类

枚举类的声明
  1. 枚举类的理解:类的对象只有有限个,确定的,称此类为枚举类
  2. 当需要定义一组常量时,强烈建议使用枚举类
  3. 如果枚举类中只有一个对象,则可以作为单例模式的实现方式
如何自定义枚举类

JDK5.0之前,自定义枚举类

// 自定义枚举类
class Season{

    // 1.声明 Season 对象的属性,private final 修饰
    private final String seasonName;
    private final String seasonDesc;

    // 2.私有化类的构造器
    private Season(String seasonName, String seasonDesc){
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }

    // 3.提供当前枚举类的多个对象:public static final 的
    public static final Season SPRING = new Season("春天", "春暖花开");
    public static final Season SUMMER = new Season("夏天", "夏日炎炎");
    public static final Season AUTUMN = new Season("秋天", "秋高气爽");
    public static final Season WINTER = new Season("冬天", "冰天雪地");

    // 4.其它诉求1:获取枚举类对象的属性
    public String getSeasonName() {
        return seasonName;
    }

    public String getSeasonDesc() {
        return seasonDesc;
    }

    // 4.其它诉求2:提供 toString()

    @Override
    public String toString() {
        return "Season{" +
                "seasonName='" + seasonName + '\'' +
                ", seasonDesc='" + seasonDesc + '\'' +
                '}';
    }
}
JDK5.0,新增使用 enum 关键字定义枚举类
// 使用 enum 关键字定义枚举类
enum Season1{
    // 1.提供当前枚举类的对象,多个对象之间用逗号隔开
    SPRING("春天", "春暖花开").
    SUMMER("夏天", "夏日炎炎"),
    AUTUMN("秋天", "秋高气爽"),
    WINTER("冬天", "冰天雪地");

    // 2.声明 Season 对象的属性,private final 修饰
    private final String seasonName;
    private final String seasonDesc;

    // 3.私有化类的构造器
    Season1(String seasonName, String seasonDesc){
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }


    // 4.其它诉求1:获取枚举类对象的属性
    public String getSeasonName() {
        return seasonName;
    }

    public String getSeasonDesc() {
        return seasonDesc;
    }
}
枚举类中的常用方法

继承于 java.lang.Enum

  • values():返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值。
  • valueOf(String str):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException。
  • toString():返回当前枚举类对象常量的名称
public static void main(String[] args) {
    Season1 summer = Season1.SUMMER;
    System.out.println(summer);
    System.out.println(Season1.class.getSuperclass());
    // values()
    Season1[] values = Season1.values();
    for (Season1 value : values) {
        System.out.println(value);
    }
    for (Thread.State value : Thread.State.values()) {
        System.out.println(value);
    }
    // valueOf(String objName):返回枚举类中对象名是 objName 的对象
    // 如果没有 objName 的枚举类对象,则抛异常 IllegalArgumentsException
    Season1 winter = Season1.valueOf("WINTER");
    System.out.println(winter);
}
使用 enum 关键字定义的枚举类实现接口的情况
  1. 情况一:实现接口,在 enum 类中实现抽象方法
  2. 情况二:让枚举类对象分别实现接口中的抽象方法
interface Info{
    void show();
}

// 使用 enum 关键字定义枚举类
enum Season1 implements Info{
    // 1.提供当前枚举类的对象,多个对象之间用逗号隔开
    SPRING("春天", "春暖花开"){
        @Override
        public void show() {
            System.out.println("春天在哪里");
        }
    },
    SUMMER("夏天", "夏日炎炎") {
        @Override
        public void show() {
            System.out.println("宁夏");
        }
    },
    AUTUMN("秋天", "秋高气爽") {
        @Override
        public void show() {
            System.out.println("秋天不回来");
        }
    },
    WINTER("冬天", "冰天雪地") {
        @Override
        public void show() {
            System.out.println("大约在冬季");
        }
    };

    // 2.声明 Season 对象的属性,private final 修饰
    private final String seasonName;
    private final String seasonDesc;

    // 3.私有化类的构造器
    Season1(String seasonName, String seasonDesc){
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }


    // 4.其它诉求1:获取枚举类对象的属性
    public String getSeasonName() {
        return seasonName;
    }

    public String getSeasonDesc() {
        return seasonDesc;
    }
}

注解

理解 Annotation
  1. JDK5.0新增

  2. Annotation 其实就是代码里的特殊标记, 这些标记可以在编译, 类加载,运行时被读取,并执行相应的处理,通过使用 Annotation, 程序员可以在不改变原有逻辑的情况下, 在源文件中嵌入一些补充信息

  3. 在 JavaSE 中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在 JavaEE / Android 中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替 JavaEE 旧版中所遗留的繁冗代码和 XML 配置等

框架 = 注解 + 反射机制 + 设计模式

Annotation的使用实例
  1. 示例1:生成文档相关的注解
  2. 示例2:在编译时进行格式检查(JDK 内置三个基本注解)
    1. @Override: 限定重写父类方法, 该注解只能用于方法
    2. @Deprecated: 用于表示所修饰的元素(类, 方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择
    3. @SuppressWarnings: 抑制编译器警告
  3. 示例3:跟踪代码依赖性,实现替代配置文件功能
如何自定义注解

参照 @SuppressWarings 定义

步骤
  1. 注解声明为 @interface
  2. 内部定义成员,通常使用 value 表示
  3. 可以指定成员的默认值,使用 default 定义
  4. 如果自定义注解没有成员,表明是一个标识作用
代码举例
@Inherited
@Repeatable(MyAnnotations.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({
        ElementType.TYPE,
        ElementType.FIELD,
        ElementType.METHOD,
        ElementType.LOCAL_VARIABLE,
        ElementType.TYPE_PARAMETER,
        ElementType.TYPE_USE
})
public @interface MyAnnotation {

    String value() default "hello";
}

说明

  1. 如果注解有成员,在使用注解时,需要指明成员的值
  2. 自定义注解必须配上注解的信息处理流程(使用反射)才有意义
  3. 自定义注解通常都会指明两个元注解:Rentention Target
元注解

元注解:对现有的注解进行解释说明的注解

  • Retention:指定所修饰的 Annotation 的生命周期:SOURCE CLASS(默认) RUNTIME,只有声明为 RUNTIME 生命周期的注解,才能提供反射获取
  • Target:用于指定被修饰的 Annotation 能用于修饰哪些程序元素
  • Documented:表示所修饰的注解在 Javadoc 解析时,保留下来
  • Inherited:被它修饰的 Annotation 将具有继承性
如何获取注解信息

通过反射获取注解信息

前提:要求此注解的元注解 Retention 中声明的生命周期状态为:RUNTIME

JDK8 中注解的新特性
可重复注解
  1. 在 MyAnnotation 上声明 @Repeatable,成员值为 MyAnnotations.class
  2. MyAnnotation 的 Target 和 Retention等元注解必须和 MyAnnotations 相同
类型注解
  1. ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中(如:泛型声明)。
  2. ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中

集合

数组与集合

集合框架的概述

集合、数组都是对多个数据进行存储操作的结构,简称 Java 容器
说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt, .jpg, .avi,数据库中)

数组在存储的特点
  • 一旦初始化以后,其长度就确定了

  • 数组一旦定义好,其元素的类型也就确定了,也就只能操作指定类型的数据了

    比如:String[] arr int[] arr;

数据存储的弊端
  • 一旦初始化以后,其长度就不可修改

  • 数组中提供方法非常有限,对于添加、删除、插入数据等操作,非常不便

  • 获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用

  • 数组存储数据的特点:有序、可重复,对于无序、不可重复的需求,数组不能满足

集合存储的优点
  • 解决数组存储数据方面的弊端

集合框架

|---- Collection 接口:单列集合,用来存储一个一个的数据
      |---- List 接口:存储有序的、可重复的数据 --> “动态"数组
            |---- ArrayList
            |---- LinkedList
            |---- Vector
      |---- Set 接口:存储无序的、不可重复的数据 --> 高中讲的"集合“
            |---- HashSet
                  |---- LinkedHashSet
            |---- TreeSet
|---- Map 接口:双列集合,用来存储一对(key : value)一对的数据 --> 高中函数: y = f(x)
      |---- HashMap
            |---- LinkedHashMap
      |---- TreeMap
      |---- HashTable
      |---- Properties

Collection 接口

单列集合框架结构
|---- Collection 接口:单列集合,用来存储一个一个的数据
      |---- List 接口:存储有序的、可重复的数据 --> “动态"数组
            |---- ArrayList
            |---- LinkedList
            |---- Vector
      |---- Set 接口:存储无序的、不可重复的数据 --> 高中讲的"集合“
            |---- HashSet
                    |---- LinkedHashSet
            |---- TreeSet

对应图示

image-20211127004906534

Collection 接口的常用方法
  • add(Object obj):将元素 obj 添加到元素集合中
  • addAll(Collection collection):将 collection 集合中的元素添加到当前的集合中
  • size():获取添加的元素的个数
  • isEmpty():判断当前集合是否为空
  • clear():清空集合元素
  • contains(Object obj):判断当前集合中是否包含 obj,在判断时会调用 obj 对象所在类的 equals()
  • containsAll(Collection collection):判断形参 collection 中的所有元素是否都存在于当前集合中
  • remove(Object obj):从当前集合中移除 obj 元素
  • removeAll(Collection collection):差集,从当前集合中移除 collection 中所有的元素
  • retainsAll(Collection collection):交集,获取当前集合和 collection 集合的交集,并返回给当前集合
  • equals(Object obj):要想返回 true,就要判断当前集合和形参集合元素都相同
  • hashCode():返回当前对象的哈希值
  • toArray():集合转换为数组
  • iterator()返回此集合中的元素的迭代器
Collection 集合与数组之间的转换
// 8.toArray():集合转换为数组
Object[] arr = collection.toArray();
for (int i = 0; i < arr.length; i++) {
    System.out.println(arr[i]);
}
// 扩展:数组转换为集合:调用 Arrays 类的静态方法 asList()
List<String> list = Arrays.asList(new String[] {"aa", "bb", "cc"});
System.out.println(list);
使用 Collection 集合存储对象,要求对象所属的类满足
  • 向 Collection 接口的实现类的对象中添加数据 obj 时,要求 obj 所在类要重写 equals()
要求
  • 层次一:选择合适的集合类去实现数据的保存,调用其内部的相关方法
  • 层次二:不同的集合类底层的数据结构为何?如何实现数据的操作的:增删改查等

Iterator 接口与 foreach 循环

遍历 Collection 的两种方式
  • 使用迭代器 Iterator
  • foreach 循环(或增强 for 循环)
java.utils 包下定义的迭代器接口:Iterator
说明
  • Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。
  • GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元 素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。
  • 内部方法:hasNext() next()
    • 集合对象每次调用 iterator() 方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
    • 内部定义了 remove(),可以在遍历的时候,删除集合中的元素,此方法不同于集合直接调用 remove()
作用

遍历集合 Collection 元素

如何获取实例

collection.iterator() 返回一个迭代器实例

遍历的代码实现
Iterator iterator = collection.iterator();
// hasNext():判断是否会有下一个元素
while (iterator.hasNext()){
    // next():指针下移;将下移以后集合位置上的元素返回
    System.out.println(iterator.next());
}
图示说明

image-20211128205706362

remove() 的使用
// 如果还未调用 next() 或在上一次调用 next 方法之后已经调用了 remove 方法,
// 再调用 remove 都会报 IllegalStateException。
// 内部定义了 remove(),可以在遍历的时候,删除集合中的元素,此方法不同于集合直接调用 remove()
@Test
public void test3() {
    Collection collection = new ArrayList();
    collection.add(123);
    collection.add(456);
    collection.add(new Person("Jerry", 20));
    collection.add(new String("Tom"));
    collection.add(false);
    Iterator iterator = collection.iterator();
    // 删除集合中 “Tom” 数据
    while (iterator.hasNext()){
        Object obj = iterator.next();
        if ("Tom".equals(obj)){
            iterator.remove();
        }
    }
    iterator = collection.iterator();
    while (iterator.hasNext()){
        System.out.println(iterator.next());
    }
}
JDK5 新特性 – 增强 for 循环:(foreach 循环)
遍历集合举例
@Test
public void test1() {
    Collection collection = new ArrayList();
    collection.add(123);
    collection.add(456);
    collection.add(new Person("Jerry", 20));
    collection.add(new String("Tom"));
    collection.add(false);
    // for(集合中元素的类型 局部变量 : 集合对象)
    // 内部仍然调用了迭代器
    for(Object obj : collection){
        System.out.println(obj);
    }
}

说明:内部仍然调用了迭代器

遍历数组举例
@Test
public void test2(){
    int[] arr = new int[]{1, 2, 3, 4, 5, 6};
    for (int a : arr){
        System.out.println(a);
    }
}

Collection 子接口:List 接口

存储的数据特点

存储有序的、可重复的数据

常用方法
  • 增:add(Object obj)
  • 删:remove(int index) / remove(Object obj)
  • 改:set(int index, Object ele)
  • 查:get(int index)
  • 插:add(int index, Object ele)
  • 长度:size()
  • 遍历
    • Iterator 迭代器方式
    • 增强 for 循环
    • 普通 for 循环
常用实现类
|---- Collection 接口:单列集合,用来存储一个一个的数据
      |---- List 接口:存储有序的、可重复的数据 --> “动态"数组,替换原有数组
            |---- ArrayList
                     作为 List 接口的主要实现类
                     线程不安全,效率高
                     底层使用 Object[] elementData 存储
            |---- LinkedList
                     对于频繁的插入和删除,使用此类效率比 ArrayList 高
                     底层使用双向列表存储
            |---- Vector
                     作为 List 接口的古老实现类
                     线程安全,效率低
                     底层使用 Object[] elementData 存储
源码分析
  • ArrayList 的源码分析

    • JDK7 情况下

      ArrayList list = new ArrayList(); // 底层创建了长度是10的 Object[] 数组 elementData
      list.add(123); // elementData[0] = new Integer(123);

      list.add(11); // 如果此次的添加导致底层 elementData 数组容量不够,则扩容,默认情况下,扩容为原来容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中

      结论:建议开发中使用带参的构造器:ArrayList list = ArrayList(int initialCapacity);

      • JDK8 中的变化

        ArrayList list = new ArrayList(); // 底层 Object[] elementData = {},并没有创建长度为10的数组

        list.add(123); // 第一次调用 add() 时,底层才创建了长度为10的数组,并将数据添加到 elementData 中

        后续的添加和扩容操作与 JDK7 无异

      • 小结

        • JDK7 中的 ArrayList 的对象创建类似于单例的饿汉式
        • JDK8 中的 ArrayList 的对象创建类似于单例的懒汉式,延迟了数组的创建,节省内存
  • LinkedList 的源码分析

    LinkedList list = new LinkedList(); // 内部声明了 Node 类型的 first 和 last 属性,默认值为 null

    list.add(123); // 将123封装到 Node 中,创建了 Node 对象

    其中,Node 定义为:体现了 LinkedList 的双向链表的说法

    private static class Node<E> {
          E item;
          Node<E> next;
          Node<E> prev;
          Node(Node<E> prev, E element, Node<E> next) {
              this.item = element;
              this.next = next;
              this.prev = prev;
          }
      }
    
    
  • Vector 的源码分析

    通过 Vector() 构造器创建对象时,底层都创建了长度为10的数组,在扩容反码,默认扩容为原来数组长度的2倍

存储的元素的要求

添加的对象所在的类要重写 equals()

面试题:ArrayList、LinkedList、Vector 三者的异同?

  • 同:三个类都是实现了 List 接口,存储数据的特点相同:存储有序的可重复的数据
    • ArrayList
      • 作为 List 接口的主要实现类
      • 线程不安全,效率高
      • 底层使用 Object[] elementData 存储
    • LinkedList
      • 对于频繁的插入和删除,使用此类效率比 ArrayList 高
      • 底层使用双向列表存储
    • Vector
      • 作为 List 接口的古老实现类
      • 线程安全,效率低
      • 底层使用 Object[] elementData 存储

Collection 子接口:Set 接口

存储数据的特点
  • 无序性
  • 不可重复性

具体的

以 HashSet 为例说明

  1. 无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定
  2. 不可重复性:保证添加的元素按照 equals() 判断时,不能返回 true,即:相同的元素只能添加一个
元素添加过程

以 HashSet 为例

  • 向 HashSet 中添加元素 a,首先调用元素 a 所在类的 hashCode() 方法,计算元素 a 的哈希值,此哈希值接着通过某种算法计算出在 HashSet 底层数组中的存放位置(即为:索引位置),判断数组此位置上是否以及有元素
    • 如果此位置上没有其它元素,则元素 a 添加成功 --> 情况1
    • 如果此位置上有其它元素 b(或以链表形式存在多个元素),则比较元素 a 与元素 b 的哈希值
      • 如果哈希值不相同,则元素 a 添加成功 --> 情况2
      • 如果哈希值相同,进而需要调用元素 a 所在类的 equals() 方法
        • equals() 返回 true,元素 a 添加失败
        • equals() 返回 false,元素 a 添加成功 --> 情况3
  • 对于添加成功的情况2和情况3而言:元素 a 与已经存在指定索引位置上数据以链表的方式存储
    • JDK7:元素 a 放到数组中,指向原来的元素
    • JDK8:原来的元素放到数组中,指向元素 a
    • 总结:七上八下
常用方法

Set 接口中没有额外定义新的方法,使用的都是 Collection 中声明过的方法

常用实现类
|---- Collection 接口:单列集合,用来存储一个一个的数据
    |---- Set 接口:存储无序的、不可重复的数据 --> 高中讲的"集合“
          |---- HashSet
                    作为 Set 接口的主要实现类
                    线程不安全的
                    可以存储 null 值
               |---- LinkedHashSet
                         作为 HashSet 的子类,在添加数据的同时,每个数据还维护了两个引用,
                         记录此数据的前一个数据和后一个数据
                         遍历其内部数据时,可以按照添加的顺序遍历
                         对于频繁的遍历操作:LinkedHashSet 效率高于 HashSet
          |---- TreeSet
                    可以按照添加对象的指定属性,进行排序
TreeSet 的使用
使用说明
  • 向 TreeSet 中添加的数据,要求是相同类的对象
  • 两种排序方式:自然排序(Comparable) 定制排序(Compatator)
常用的排序方式
  • 自然排序

    @Test
    public void test1(){
        TreeSet set = new TreeSet();
        // 失败:不能添加不同类的对象
        // set.add(456);
        // set.add(123);
        // set.add("AA");
        // set.add("CC");
        // set.add(new User("Tom", 12));
    
        // set.add(34);
        // set.add(-34);
        // set.add(43);
        // set.add(11);
        // set.add(8);
    
        set.add(new User("Tom", 12));
        set.add(new User("Jerry", 32));
        set.add(new User("Jim", 22));
        set.add(new User("Mike", 65));
        set.add(new User("Jack", 33));
        set.add(new User("Jack", 56));
    
    
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
    
  • 定制排序

    @Test
    public void test2() {
        Comparator comparator = new Comparator() {
            /**
             * 按照年龄从小到大排列
             * @param o1
             * @param o2
             * @return
             */
            @Override
            public int compare(Object o1, Object o2) {
                if (o1 instanceof User && o2 instanceof User){
                    User u1 = (User) o1;
                    User u2 = (User) o2;
                    return Integer.compare(u1.getAge(), u2.getAge());
                }
                throw new RuntimeException("输入的数据类型不匹配");
            }
        };
        TreeSet set = new TreeSet(comparator);
        set.add(new User("Tom", 12));
        set.add(new User("Jerry", 32));
        set.add(new User("Jim", 22));
        set.add(new User("Mike", 65));
        set.add(new User("Jack", 33));
        set.add(new User("Jack", 56));
    
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
    
存储的元素的要求
  • HashSet / LinkedHashSet

    • 向 Set(HashSet / LinkedHashSet) 中添加的数据,其所在的类一定要重写 hashCode() 和 equals()

    • 要求

      • 重写的 hashCode() 和 equals() 尽可能保持一致性:相等的对象必须具有相等的散列码

      • 重写两个方法的小技巧:对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。

  • TreeSet

    • 自然排序中,比较两个对象是否相同的标准为:compareTo() 返回0,不再是 equals()
    • 定制排序中,比较两个对象是否相同的标准为:compare() 返回0,不再是 equals()

Map 接口

常用实现类结构
|---- Map:双列数据,存储 key-value 对的数据 --> 类似于高中函数:y = f(x)
      |---- HashMap
                作为 Map 的主要实现类
                线程不安全的,效率高
                存储 null 的 key 和 value
            |---- LinkedHashMap
                        保证在遍历 map 元素时,可以按照添加的顺序实现遍历
                        原因:在原有的 HashMap 底层结构基础上,添加了一对指针,指向前一个和后一个元素
                        对于频繁的遍历操作,此类执行效率高于 HashMap
      |---- TreeMap
                保证按照添加的 key-value 对进行排序,实现排序遍历,此时考虑 key 的自然排序和定制排序
                底层使用红黑树
      |---- Hashtable
                作为 Map 的古老实现类
                线程安全的,效率低
                不能存储 null 的 key 和 value
            |---- Properties
                      常用来处理配置文件
                      key 和 value 都是 String 类型

HashMap 的底层:

  • JDK7及之前:数组 + 链表
  • JDK8:数组 + 链表 + 红黑树

面试题

  • HashMap 的底层实现原理
  • HashMap 和 Hashtable 的异同
  • CurrentHashMap 和 Hashtable 的异同(暂时不讲)
存储结构的理解

Map 中的 key:无序的、不可重复的,使用 Set 存储所有的 key --> key 所在的类要重写 equals() 和 hashCode()

Map 中的 value:有序的、可重复的,使用 Collection 存储所有的 value --> value 所在的类要重写 equals()

一个键值对:key-value 构成了一个 Entry 对象

Map 中的 Entry:无序的、不可重复的,使用 Set 存储所有的 entry

图示

image-20211129233638476

常用方法
  • 增:put(Object key,Object value)
  • 删:remove(Object key)
  • 改:put(Object key,Object value)
  • 查:get(Object key)
  • 长度:size()
  • 遍历:
    • keySet()
    • values()
    • entrySet()
内存结构说明
HashMap 在 JDK7 中实现原理
  • HashMap map = new HashMap();
    在实例化以后,底层创建了长度是16的一维数组 Entry[] table
  • …可能已经执行过多次…
  • map.put(key1, value1)
    首先,调用 key1 所在类的 hashCode() 计算 key1 的哈希值,此哈希值经过某种算法计算以后,得到在 Entry 数组中存放的位置
    • 如果此位置上的数据为空,此时的 key1-value1 添加成功 --> 情况1
    • 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较 key1 和已经存在的一个或多个数据的哈希值
      • 如果 key1 的哈希值与已经存在的数据的哈希值都不相同,此时的 key1-value1 添加成功 --> 情况2
      • 如果 key1 的哈希值与已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用 key1 所在类的 equals(key2) 方法,比较
        • 如果 equals() 返回 false:此时的 key1-value1 添加成功 --> 情况3
        • 如果 equals() 返回 true:使用 value1 替换 value2

关于情况2和情况3:此时的 key1-value1 和原来的数据以链表的方式存储

在不断的添加过程中,会涉及到扩容问题,默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来

HashMap 在 JDK8 相较于 JDK7 在底层实现方面的不同
  • new HashMap():底层没有创建一个长度为16的数组
  • JDK8 底层的数组是:Node[],而非 Entry[]
  • 首次调用 put() 方法时,底层创建长度为16的数组
  • JDK7 底层结构只有:数组 + 链表,JDK8 中底层结构:数组 + 链表 + 红黑树
    • 形成链表时,七上八下
      • JDK7:新的元素指向旧的元素
      • JDK8:旧的元素指向新的元素
    • 当数组的某一个索引位置上的元素以链表形式存在的数据个数大于8且当前数组长度超过64时,此时此索引位置上的所有数据改为使用红黑树存储
HashMap底层典型属性的说明
  • DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
  • DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
  • threshold:扩容的临界值,= 容量 * 填充因子 16 * 0.75 = 12
  • TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
  • MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
LinkedHashMap 的底层实现原理

LinkedHashMap 底层使用的结构与 HashMap 相同,因为 LinkedHashMap 继承于 HashMap,区别就在于:LinkedHashMap 内部提供了 Entry,替换 HashMap 中的 Node

HashMap 中的 Node:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

LinkedHashMap 中的 Entry

static class Entry<K,V> extends HashMap.Node<K,V> {
      Entry<K,V> before, after; // 能够记录添加元素的先后顺序
      Entry(int hash, K key, V value, Node<K,V> next) {
          super(hash, key, value, next);
      }
  }
TreeMap 的使用

向 TreeMap 中添加 key-value,要求 key 必须是有同一个类创建的对象,因为要按照 key 进行排序:自然排序 定制排序

使用 Properties 读取配置文件

常用于处理属性文件,key 和 value 都是字符串类型

public static void main(String[] args)  {
    FileInputStream fileInputStream = null;
    try {
        Properties prop = new Properties();
        fileInputStream = new FileInputStream("jdbc.properties");
        prop.load(fileInputStream);
        String name = prop.getProperty("name");
        String password = prop.getProperty("password");
        System.out.println("name = " + name);
        System.out.println("password = " + password);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (fileInputStream != null) {
            try {
                fileInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

Collections 工具类的使用

作用

操作 Collection 和 Map 的工具类

常用方法
  • reverse(List):反转 List 中元素的顺序
  • shuffle(List):对 List 集合元素进行随机排序
  • sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
  • sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
  • swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
  • Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
  • Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
  • Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素
  • Object min(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最小元素
  • int frequency(Collection,Object):返回指定集合中指定元素的出现次数
  • void copy(List dest, List src):将 src 中的内容复制到 dest 中
  • boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值

image-20211130004949111

说明:ArrayList 和 HashMap 都是线程不安全的,如果程序要求线程安全,可以将 ArrayList 和 HashMap转换为线程安全的,使用 synchronizedList(List list)synchronizedMap(Map map)

面试题

Collection 和 Collections 的区别

数据结构简述

概述

数据结构(Data Structure)是一门和计算机硬件与软件都密切相关的学科,它的研究重点是在计算机的程序设计领域中探讨如何在计算机中组织和存储数据并进行高效率的运用,涉及的内容包含:数据的逻辑关系、数据的存储结构、排序算法(Algorithm)、查找(或搜索)等。

数据结构和算法的理解

**序能否快速而高效地完成预定的任务,取决于是否选对了数据结构,而程序是否能清楚而正确地把问题解决,则取决于算法。**算法是计算机处理信息的本质,因为计算机程序本质上是一个算法来告诉计算机确切的步骤来执行一个指定的任务。

所以大家认为:“Algorithms + Data Structures = Programs”(出自:Pascal之父Nicklaus Wirth)

总结:算法是为了解决实际问题而设计的,数据结构是算法需要处理的问题载体。

数据结构的研究对象
数据间的逻辑结构
image-20211130010032591
数据的存储结构
  • 线性表(顺序表、链表、栈、队列)

说明

  • 习惯上把顺序表和链表看做基本数据结构(或真实数据结构)
  • 习惯上把栈、队列、树、图看成抽象数据类型,简称 ADT

泛型

泛型的理解

泛型的概念

所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类 型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如, 继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实 际的类型参数,也称为类型实参)。

泛型的引入背景

集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的 对象,所以在JDK1.5之前只能把元素类型设计为Object,JDK1.5之后使用泛型来 解决。因为这个时候除了元素的类型不确定,其他的部分是确定的,例如关于 这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个 参数,这个类型参数叫做泛型。Collection,List,ArrayList 这个就是类型参数,即泛型。

泛型在集合中的使用

集合中使用泛型之前的例子
@Test
public void test1(){
    ArrayList list = new ArrayList();
    list.add(78);
    list.add(77);
    list.add(89);
    list.add(88);
    // 问题一:类型不安全
    // list.add("Tom");
    for (Object score : list) {
        // 问题二:强转时,可能出现 ClassCastException
        int stuScore = (int) score;
        System.out.println(stuScore);
    }
}

图示

image-20211201150357537

在集合中使用泛型例子1
@Test
public void test2(){
    // ArrayList<Integer> list = new ArrayList<Integer>();
    // JDK7 新特性:类型推断
    ArrayList<Integer> list = new ArrayList<>();
    list.add(78);
    list.add(87);
    list.add(99);
    list.add(65);
    // 编译时,就会进行类型检查,保证数据的安全
    // list.add("65");
    /*for(Integer score : list){
        // 避免了强转操作
        int stuScore = score;
        System.out.println(stuScore);
    }*/
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()){
        int stuScore = iterator.next();
        System.out.println(stuScore);
    }
}

图示

image-20211201150600981

在集合中使用泛型例子2
@Test
public void test3(){
    Map<String, Integer> map = new HashMap<>();
    map.put("Tom", 87);
    map.put("Jerry", 87);
    map.put("Jack", 67);
    // map.put(123, "67");
    Set<Map.Entry<String, Integer>> entry = map.entrySet();
    Iterator<Map.Entry<String, Integer>> iterator = entry.iterator();
    while (iterator.hasNext()){
        Map.Entry<String, Integer> e = iterator.next();
        String key = e.getKey();
        Integer value = e.getValue();
        System.out.println(key + " --- " + value);
    }
}
集合中使用泛型总结
  • 集合接口或集合类在 JDK5.0 时都修改为带泛型的结构
  • 在实例化集合类时,可以指明具体的泛型类型
  • 指明完以后,在集合类或接口中凡是定义类或接口时,内部结构(方法,构造器,属性等)使用到类的泛型的位置,都指定为实例化的泛型类型。比如:add(E e) --> 实例化以后:add(Integer e)
  • 注意点:泛型的类型必须是类,不能是基本数据类型,需要用到基本数据类型的位置,拿包装类替换
  • 如果实例化时没有指明泛型的类型,默认类型为 java.lang.Object 类型

自定义泛型类、泛型接口、泛型方法

举例

[Order.java]

public class Order<T> {

    String orderName;
    int orderId;

    // 类的内部结构就可以使用类的泛型
    T orderT;

    public Order(){
        // 编译不通过
        // T[] arr = new T[10];
        // 编译通过
        T[] arr = (T[]) new Object[10];
    }

    public Order(String orderName, int orderId, T orderT) {
        this.orderName = orderName;
        this.orderId = orderId;
        this.orderT = orderT;
    }

    public String getOrderName() {
        return orderName;
    }

    public void setOrderName(String orderName) {
        this.orderName = orderName;
    }

    public int getOrderId() {
        return orderId;
    }

    public void setOrderId(int orderId) {
        this.orderId = orderId;
    }

    public T getOrderT() {
        return orderT;
    }

    public void setOrderT(T orderT) {
        this.orderT = orderT;
    }

    @Override
    public String toString() {
        return "Order{" +
                "orderName='" + orderName + '\'' +
                ", orderId=" + orderId +
                ", orderT=" + orderT +
                '}';
    }

    // 静态方法中不能使用类的泛型
    /*public static void show(){
        System.out.println(orderT);
    }*/

    public void show(){
        // 编译不通过
       /*try {

       }catch (T t){

       }*/
    }

    // 泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系
    // 换句话说,泛型方法所属的类是不是泛型类都没有关系
    // 泛型方法可以声明为静态的,原因:泛型参数是在调用方法时确定的,并非在实例化类时确定
    public static <E> List<E> copyFromArrayToList(E[] arr){
        ArrayList<E> list = new ArrayList<>();
        for (E e : arr) {
            list.add(e);
        }
        return list;
    }
}

[SubOrder.java]

public class SubOrder extends Order<Integer> {
    public static <E> List<E> copyFromArrayToList(E[] arr){
        ArrayList<E> list = new ArrayList<>();
        for (E e : arr) {
            list.add(e);
        }
        return list;
    }
}

[SubOrder1.java]

public class SubOrder1<T> extends Order<T>{
}

测试

public class GenericTest1 {

    @Test
    public void test1(){
        // 如果定义了泛型类,实例化没有指明类的泛型,则认为此泛型类型为 Object 类型
        // 要求:如果定义了类是带泛型的,建议在实例化时要指明类的泛型
        Order order = new Order();
        order.setOrderT(123);
        order.setOrderT("123");
        // 建议:实例化时指明类的泛型
        Order<String> order1 = new Order<>("orderAA",
                1001, "order:AA") ;
        order1.setOrderT("AA:hello");
    }

    @Test
    public void test2(){
        // 由于子类在继承带泛型的父类时,指明了泛型类型,则实例化子类对象时,不再需要指明泛型
        SubOrder subOrder = new SubOrder();
        subOrder.setOrderT(1122);

        SubOrder1<String> subOrder1 = new SubOrder1<>();
        subOrder1.setOrderT("order2...");
    }

    @Test
    public void test3(){
        // 泛型不同的引用不能相互赋值
        ArrayList<String> list1 = null;
        ArrayList<Integer> list2 = new ArrayList<>();
        // list1 = list2;

        Person p1 = null;
        Person p2 = null;
        p1 = p2;
    }

    // 测试泛型方法
    @Test
    public void test4(){
        Order<String> order = new Order<>();
        Integer[] arr = {1, 2, 3, 4};
        // 泛型方法在调用时,指明泛型参数的类型
        List<Integer> list = order.copyFromArrayToList(arr);
        System.out.println(list);
    }
}
注意点
  1. 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:
    <E1,E2,E3>

  2. 泛型类的构造器如下:public GenericClass(){}。 而下面是错误的:public GenericClass(){}

  3. 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。

  4. 泛型不同的引用不能相互赋值。

    尽管在编译时ArrayList和ArrayList是两种类型,但是,在运行时只有
    一个ArrayList被加载到JVM中。

  5. 泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价 于Object。经验:泛型要使用一路都用。要不用,一路都不要用。

  6. 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。

  7. jdk1.7,泛型的简化操作:ArrayList flist = new ArrayList<>();

  8. 泛型的指定中不能使用基本数据类型,可以使用包装类替换。

  9. 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态 属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法 中不能使用类的泛型。

  10. 异常类不能是泛型的

  11. 不能使用new E[]。但是可以:E[] elements = (E[])new Object[capacity];
    参考:ArrayList源码中声明:Object[] elementData,而非泛型参数类型数组。

  12. 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:

    1. 子类不保留父类的泛型:按需实现
      1. 没有类型 擦除
      2. 具体类型
    2. 子类保留父类的泛型:泛型子类
      1. 全部保留
      2. 部分保留

    结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型

应用场景举例

[DAO.java]:定义了操作数据库中的表的通用操作。ORM 思想(数据库中的表和 Java 中的类对应)

public class DAO<T> {

    // 添加一条记录
    public void add(T t){

    }

    // 删除一条记录
    public boolean remove(int index){
        return false;
    }

    // 修改一条记录

    public void update(int index, T t){

    }

    // 查询一条记录
    public T getIndex(int index){
        return null;
    }

    // 查询多条记录
    public List<T> getForList(int index){
        return null;
    }

    public <E> E getValue(){
        return null;
    }
}

[CustomerDAO.java]

public class CustomerDAO extends DAO<Customer>{
}

[StudentDAO.java]

public class StudentDAO extends DAO<Student>{
}

泛型在继承上的体现

虽然类 A 是类 B 的父类,但是 G 和 G 二者不具备子父类关系,二者是并列关系

补充:类 A 是类 B 的父类,A 是 B 的父类

@Test
public void test1(){
    Object obj = null;
    String str = null;
    obj = str;
    Date date = new Date();
    // 编译不通过
    // str = date;

    Object[] arr1 = null;
    String[] arr2 = null;
    arr1 = arr2;

    List<Object> list1 = null;
    List<String> list2 = new ArrayList<>();
    // 此时的 list1 和 list2 的类型不具备子父类关系
    // 编译不通过
    // list1 = list2;
    /*
        反证法
        假设 list1 = list2; // 导致混入非 String 的数据,出错
     */
    show(list1);
    // show(list2);
}

public void show(List<Object> list){

}

@Test
public void test2(){
    AbstractList<String> list1 = null;
    List<String> list2 = null;
    ArrayList<String> list3 = null;
    list1 = list3;
    list2 = list3;
}

通配符

通配符的使用

通配符:?

类 A 是类 B 的父类,G 和 G 是没有关系的,二者共同的父类是:G<?>

@Test
public void test3() {
    List<Object> list1 = null;
    List<String> list2 = null;

    List<?> list = null;

    list = list1;
    list = list2;
    // print(list1);
    // print(list2);

    List<String> list3 = new ArrayList<>();
    list3.add("AA");
    list3.add("BB");
    list3.add("CC");
    list = list3;
    // 添加(写入):对于 List<?> 就不能向其内部添加数据,除了添加 null 之外
    // list.add("DD");
    list.add(null);

    // 获取(读取):允许读取数据,读取的数据类型为 Object
    Object o = list.get(0);
    System.out.println(o);
}

public void print(List<?> list){
    Iterator<?> iterator = list.iterator();
    while (iterator.hasNext()){
        Object obj = iterator.next();
        System.out.println(obj);
    }
}
涉及到通配符的集合的数据的写入和读取
@Test
public void test3() {
    List<Object> list1 = null;
    List<String> list3 = new ArrayList<>();
    list3.add("AA");
    list3.add("BB");
    list3.add("CC");
    list = list3;
    // 添加(写入):对于 List<?> 就不能向其内部添加数据,除了添加 null 之外
    // list.add("DD");
    list.add(null);

    // 获取(读取):允许读取数据,读取的数据类型为 Object
    Object o = list.get(0);
    System.out.println(o);
}
有限制条件的通配符的使用
  • ? extends A:G<? extends A> 可以作为 G 和 G 的父类,其中 B 是 A 的子类
  • ? super Person:G<? super A> 可以作为 G 和 G 的父类,其中 B 是 A 的父类
@Test
public void test4(){
    List<? extends Person> list1 = null;
    List<? super Person> list2 = null;
    List<Student> list3 = new ArrayList<>();
    List<Person> list4 = new ArrayList<>();
    List<Object> list5 = new ArrayList<>();
    list1 = list3;
    list1 = list4;
    // list1 = list5;

    // list2 = list3;
    list2 = list4;
    list2 = list5;

    // 读取数据
    list1 = list3;
    Person person = list1.get(0);
    // 编译不通过
    // Person person1 = list1.get(0);

    list2 = list4;
    Object obj = list2.get(0);

    // 写入数据:
    // list1.add(new Student());
    list2.add(new Person());
    list2.add(new Student());
}

IO 流

File 类的使用

File 类的理解
  • File 类的一个对象,代表一个文件或一个目文件录
  • File 类声明在 java.io 包下
  • File 类中涉及到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法,并未涉及到写入或读取文件内容的操作,如果需要读取或写入文件内容,必须使用 IO 流来完成
  • 后续 File 类的对象常会作为参数传递到流的构造器中,指明读取或写入的“终点”
File 的实例化
常用构造器
  • File(String filepath)
  • File(String parentPath, String childPath)
  • File(File parentFile, String childPath)
路径的分类
  • 相对路径:相较于某个路径下,指明的路径
  • 绝对路径:包含盘符在内的文件或文件目录的路径

说明:

  • IDEA

    • 如果开发使用 JUnit 中的 单元测试方法测试,相对路径即为当前 module 下
    • 如果开发使用 main() 方法测试,相对路径即为当前 Project 下
  • Eclipse

    • 不管使用单元测试方法还是使用 main() 方法测试,相对路径都是为当前 Project 下
路径分隔符
  • windows 和 DOS 系统默认使用“\”来表示
  • UNIX 和 URL 使用“/”来表示
File 类的常用方法
  • File类的获取功能

    • public String getAbsolutePath():获取绝对路径

    • public String getPath():获取路径

    • public String getName():获取名称

    • public String getParent():获取上层文件目录路径。若无,返回 null

    • public long length():获取文件长度(即:字节数)。不能获取目录的长度。

    • public long lastModified():获取最后一次的修改时间,毫秒值

      如下的两个方法适用于文件目录

    • public String[] list():获取指定目录下的所有文件或者文件目录的名称数组

    • public File[] listFiles():获取指定目录下的所有文件或者文件目录的 File 数组

  • File类的重命名功能

    • public boolean renameTo(File dest):把文件重命名为指定的文件路径
  • File类的判断功能

    • public boolean isDirectory():判断是否是文件目录
    • public boolean isFile():判断是否是文件
    • public boolean exists():判断是否存在
    • public boolean canRead():判断是否可读
    • public boolean canWrite():判断是否可写
    • public boolean isHidden():判断是否隐藏
  • File 类的创建功能

    • public boolean createNewFile():创建文件。若文件存在,则不创建,返回false
    • public boolean mkdir():创建文件目录。如果此文件目录存在,就不创建了。 如果此文件目录的上层目录不存在,也不创建。
    • public boolean mkdirs():创建文件目录。如果上层文件目录不存在,一并创建

    注意事项:如果你创建文件或者文件目录没有写盘符路径,那么,默认在项目 路径下。

  • File 类的删除功能

    • public boolean delete():删除文件或者文件夹

      删除注意事项:Java中的删除不走回收站。 要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录

IO流概述

流的分类
  • 操作数据单位

    • 字节流
    • 字符流
  • 数据的流向

    • 输入流
    • 输出流
  • 流的角色

    • 节点流
    • 处理流

图示

image-20211202211011323

流的体系结构
分类字节输入流字节输出流字符输入流字符输出流
抽象基类InputStreamOutputStreamReaderWriter
访问文件FileInputStreamFileOutputStreamFileReaderFileWriter
访问数组ByteArrayInputStreamByteArrayOutputStreamCharArrayReaderCharArrayWriter
访问管道PipedInputStreamPipedOutputStreamPipedReaderPipedWriter
访问字符串StringReaderStringWriter
缓冲流BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter
转换流InputStreamReaderOutPutStreamWriter
对象流ObjectInputStreamObjectOutputStream
打印流PrintStreamPrintWriter
推回输入流PushbackInputStreamPushbackReader
特殊流DataInputStreamDataOutputStream
重点说明的几个流结构
抽象基类文件流缓冲流
InputStreamFileInputStream(read(byte[] buffer))BufferedInputStream(read(byte[] buffer))
OutputStreamFileOutputStream(write(byte[] buffer, 0, len))BufferedInputStream(write(byte[] buffer, 0, len) / flush())
ReaderFileReader(read(char[] cbuf))BufferedReader(read(char[] cbuf) / readLine())
WriterFileWriter(write(char[] cbuf, 0, len))BufferedWriter(write(char[] cbuf, 0, len) / flush())
输入输出的标准化过程
输入过程
  1. 创建 File 类的对象,指明数据的来源(要求此文件一定存在)
  2. 创建相应的输入流,将 File 类的对象作为参数,传入流的构造器中
  3. 具体的读入过程:创建相应的 byte[] 或 char[]
  4. 关闭流资源

说明:程序中出现的异常需要使用 try-catch-finally 处理

输出过程
  1. 创建 File 类的对象,指明写出数据的位置(不要求此文件一定存在)
  2. 创建相应的输出流,将 File 类的对象作为参数,传入流的构造器中
  3. 具体的写出过程:write(char[] / byte[] buffer, 0, len)
  4. 关闭流资源

说明:程序中出现的异常需要使用 try-catch-finally 处理

节点流(或文件流)

FileReader / FileWrite 的使用
FileReader 的使用

说明点

  • read() 的理解:返回读入的一个字符,如果达到文件末尾,返回-1
  • 异常的处理:为了保证流资源一定可以执行关闭操作,需要使用 try-catch-finally 处理
  • 读入的文件一定要存在,否则就会报 FileNotFoundException
@Test
public void testFileReader1() {
    FileReader fileReader = null;
    try {
        // 1.File 类的实例化
        File file = new File("hello.txt");
        // 2.FileReader 流的实例化
        fileReader = new FileReader(file);
        // 3.读入的操作
        // read(char[] cbuf):返回每次读入 cbuf 数组中的字符的个数,如果达到文件末尾,返回-1
        char[] cbuf = new char[5];
        int len = 0;
        while ((len = fileReader.read(cbuf)) != -1){
            // 错误的写法
            /*for (int i = 0; i < cbuf.length; i++) {
                System.out.print(cbuf[i]);
            }*/
            // 正确的写法
            /*for (int i = 0; i < len; i++) {
                System.out.print(cbuf[i]);
            }*/
            // 错误的写法
            // System.out.print(new String(cbuf));
            // 正确的写法
            System.out.print(new String(cbuf, 0, len));
        }
    } catch (IOException e){
        e.printStackTrace();
    } finally{
        // 4.资源的关闭
        if (fileReader != null) {
            try {
                fileReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
FileWriter 的使用

说明

  • 输出操作,对应的 File 可以不存在,并不会报异常
  • File 对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建此文件
  • File 对应的硬盘中的文件如果存在:
    • 如果流使用的构造器是:FileWriter(file, false) / FileWriter(file):对原有文件覆盖
    • 如果流使用的构造器是:FileWriter(file, true):不会对原有文件覆盖,而是在原有文件基础上追加内容
@Test
public void testFileWriter(){
    FileWriter fileWriter = null;
    try {
        // 1.提供 File 类的对象,指明写出到的文件
        File file = new File("hello1.txt");
        // 2.提供 FileWriter 的对象,用于数据的写出
        fileWriter = new FileWriter(file, false);
        // 3.写出的操作
        fileWriter.write("I have a dream!\n");
        fileWriter.write("you need to have a dream!");
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 4.流资源的关闭
        if (fileWriter != null) {
            try {
                fileWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
文本文件的复制
@Test
public void testFileReaderFileWriter() {
    FileReader fileReader = null;
    FileWriter fileWriter = null;
    try {
        // 1.创建 File 类的对象,指明读入和写出的文件
        // File srcFile = new File("hello.txt");
        // File destFile = new File("hello2.txt");
        // 不能使用字符流来处理图片等字节数据
        File srcFile = new File("爱情与友情.png");
        File destFile = new File("爱情与友情1.png");

        // 2.创建输入流和输出流的对象
        fileReader = new FileReader(srcFile);
        fileWriter = new FileWriter(destFile);

        // 3.数据的读入和写出操作
        char[] cbuf = new char[5];
        // 记录每次读入到 cbuf 数组中的字符的个数
        int len = 0;
        while ((len = fileReader.read(cbuf)) != -1){
            // 每次写出 len 个字符
            fileWriter.write(cbuf, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 4.关闭流资源
        if (fileWriter != null) {
            try {
                fileWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (fileReader != null) {
            try {
                fileReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
FileInputStream / FileOutputStream 的使用
  • 对于文本文件(.txt, .java, .c, .cpp),使用字符流处理
  • 对于非文本文件(.jpg, .mp3, .mp4, .avi, .doc, .ppt, …),使用字节流处理
/**
 * 实现对图片的复制
 */
@Test
public void testFileInputOutputStream(){
    FileInputStream fileInputStream = null;
    FileOutputStream fileOutputStream = null;
    try {
        File srcFile = new File("爱情与友情.png");
        File destFile = new File("爱情与友情2.png");

        fileInputStream = new FileInputStream(srcFile);
        fileOutputStream = new FileOutputStream(destFile);

        // 复制的过程
        byte[] buffer = new byte[5];
        int len = 0;
        while ((len = fileInputStream.read(buffer)) != -1){
            fileOutputStream.write(buffer, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }finally {
        if (fileInputStream != null) {
            try {
                fileInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (fileOutputStream != null) {
            try {
                fileOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

注意点:相对路径在 IDEA 和 Eclipse 中使用的区别?

  • IDEA
    • 如果使用单元测试方法,相对路径基于当前 Module 的
    • 如果使用 main(),相对路径基于当前 Project 的
  • Eclipse:不管是单元测试方法还是 main(),相对路径都是基于当前 Project 的

缓冲流的使用

缓冲流涉及到的类
  • BufferedInputStream
  • BufferedOutputStream
  • BufferedReader
  • BufferedWriter
作用

作用:提高流的读取、写入的速度

提高读写速度的原因:内部提供了一个缓冲区,默认情况下是8kb

public class BufferedInputStream extends FilterInputStream {
    private static int DEFAULT_BUFFER_SIZE = 8192;
}
典型代码
使用 BufferedInputStream 和 BufferedOutputStream

处理非文本文件

/**
 * 实现文件复制的方法
 */
public void copyFileWithBuffered(String srcPath, String destPath){
    BufferedInputStream bufferedInputStream = null;
    BufferedOutputStream bufferedOutputStream = null;
    try {
        // 造文件
        File srcFile = new File(srcPath);
        File destFile = new File(destPath);
        // 2.造流
        // 2.1.造节点流
        FileInputStream fileInputStream = new FileInputStream(srcFile);
        FileOutputStream fileOutputStream = new FileOutputStream(destFile);
        // 2.2.造缓冲流
        bufferedInputStream = new BufferedInputStream(fileInputStream);
        bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
        // 3.复制的细节
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = bufferedInputStream.read(buffer)) != -1){
            bufferedOutputStream.write(buffer, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 4.资源关闭
        // 要求:先关闭外层的流,再关闭内层的流
        if (bufferedOutputStream != null) {
            try {
                bufferedOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (bufferedInputStream != null) {
            try {
                bufferedInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        // 说明:在关闭外层流的同时,内层流也会自动进行关闭,对于内层流的关闭,可以省略
        // fileOutputStream.close();
        // fileInputStream.close();
    }
}
使用 BufferedReader 和 BufferedWriter

处理文本文件

/**
 * 使用 BufferedReader 和 BufferedWriter 实现文本文件的复制
 */
@Test
public void testBufferedReaderBufferedWriter(){
    BufferedReader bufferedReader = null;
    BufferedWriter bufferedWriter = null;
    try {
        // 创建文件和相应的流
        bufferedReader = new BufferedReader(new FileReader(new File("dbcp.txt")));
        bufferedWriter = new BufferedWriter(new FileWriter(new File("dbcp1.txt")));

        // 读写操作
        // 方式一,使用 char[] 数组
        /*char[] cbuf = new char[1024];
        int len = 0;
        while ((len = bufferedReader.read(cbuf)) != -1){
            bufferedWriter.write(cbuf, 0, len);
        }*/
        // 方式二:使用 String
        String data;
        while ((data = bufferedReader.readLine()) != null){
            // 方法一:
            // data 中不包含换行符
            // bufferedWriter.write(data + "\n");
            // 方法二:
            bufferedWriter.write(data);
            // 提供换行的操作
            bufferedWriter.newLine();
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (bufferedReader != null) {
            try {
                bufferedReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (bufferedWriter != null) {
            try {
                bufferedWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

转换流的使用

转换流涉及到的类

属于字符流

  • InputStreamReader:将一个字节的输入流转换为字符的输入流

    解码:字节、字节数组 --> 字符数组、字符串

  • OutputStreamWriter:将一个字符的输出流转换为字节的输出流

    编码:字符数组、字符串 --> 字节、字节数组

说明:编码决定了解码的方式

作用

提供字节流与字符流之间的转换

图示

image-20211202224022254

典型实现
@Test
public void test1(){
    InputStreamReader inputStreamReader = null;
    try {
        FileInputStream fileInputStream = new FileInputStream("dbcp.txt");
        // 使用系统默认的字符集
        // InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);
        // 参数2指明了字符集:具体使用哪个字符集,取决于文件 dbcp.txt 保存时使用的字符集
        inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
        char[] cbuf = new char[20];
        int len = 0;
        while ((len = inputStreamReader.read(cbuf)) != -1){
            System.out.print(new String(cbuf,0, len));
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (inputStreamReader != null) {
            try {
                inputStreamReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
/**
 * 综合使用 InputStreamReader 和 OutputStreamWriter
 */
@Test
public void test2(){
    InputStreamReader inputStreamReader = null;
    OutputStreamWriter outputStreamWriter = null;
    try {
        File file1 = new File("dbcp.txt");
        File file2 = new File("dbcp_gbk.txt");

        FileInputStream fileInputStream = new FileInputStream(file1);
        FileOutputStream fileOutputStream = new FileOutputStream(file2);

        inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
        outputStreamWriter = new OutputStreamWriter(fileOutputStream, "gbk");

        char[] cbuf = new char[20];
        int len = 0;
        while ((len = inputStreamReader.read(cbuf)) != -1){
            outputStreamWriter.write(cbuf, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }finally {
        if (inputStreamReader != null) {
            try {
                inputStreamReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (outputStreamWriter != null) {
            try {
                outputStreamWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
说明

文件编码的方式,决定了解析时使用的字符集

编码表
常见编码表
  • ASCII:美国标准信息交换码,用一个字节的7位可以表示。
  • ISO8859-1:拉丁码表。欧洲码表,用一个字节的8位表示。
  • GB2312:中国的中文编码表。最多两个字节编码所有字符
  • GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
  • Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示。
  • UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。
启示

客户端 / 游览器端 <—> 后台(Java, Go, Python, Node.js, PHP) <—> 数据库

要求前前后后使用的字符集都要统一:UTF-8

其它的流的使用

标准的输入输出流
  • System.in:标准的输入流,默认从键盘输入
  • System.out:标准的输出流,默认从控制台输出

修改默认的输入和输出行为:System 类的 setIn(InputStream InputStream) / setOut(OutputStream outputStream) 方式重新指定输入和输出的流

打印流
  • PrintStream
  • PrintWriter

说明

  • 提供了一系列重载的 print() 和 println() 方法,用于多种数据类型的输出
  • System.out 返回的是 PrintStream 的实例
数据流
  • DataInputStream
  • DataOutputStream

作用:用于读取或写出基本数据类型的变量或字符串

示例代码

/**
 * 数据流:DataInputStream 和 DataOutputStream
 * 1.作用:用于读取或写出基本数据类型的变量或字符串
 */
@Test
public void test3(){
    DataOutputStream dataOutputStream = null;
    try {
        dataOutputStream = new DataOutputStream(new FileOutputStream("data.txt"));
        dataOutputStream.writeUTF("刘建辰");
        dataOutputStream.writeInt(23);
        dataOutputStream.writeBoolean(true);
        // 刷新操作,将内存中的数据写入文件
        dataOutputStream.flush();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (dataOutputStream != null) {
            try {
                dataOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
/**
 * 将文件中存储的基本数据类型变量和字符串读取到内存中
 * 注意点:读取不同类型数据的顺序要以当初写入文件时,保存的数据的顺序一致
 */
@Test
public void test4(){
    DataInputStream dataInputStream = null;
    try {
        dataInputStream = new DataInputStream(new FileInputStream("data.txt"));
        System.out.println(dataInputStream.readUTF());
        System.out.println(dataInputStream.readInt());
        System.out.println(dataInputStream.readBoolean());
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (dataInputStream != null) {
            try {
                dataInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

对象流的使用

RandomAccessFile 的使用

Path、Paths、Files 的使用

项目

项目要求

  • 至少独立完成一遍以上的项目代码
  • 积累完成项目的过程中常见的 bug 调试
    • “硬”看,必要时添加输出语句
    • Debug
  • 捋顺思路,强化逻辑
  • 对象、数组等内存结构的解析
  • 遵守编码的规范,标识符的命名规范等
  • 在类前,方法前,方法内具体逻辑的实现步骤等添加必要的注释
    • 类前,方法前,属性前:文档注释
    • 逻辑步骤:单行、多行注释
      m = new FileInputStream(srcFile);
      FileOutputStream fileOutputStream = new FileOutputStream(destFile);
      // 2.2.造缓冲流
      bufferedInputStream = new BufferedInputStream(fileInputStream);
      bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
      // 3.复制的细节
      byte[] buffer = new byte[1024];
      int len = 0;
      while ((len = bufferedInputStream.read(buffer)) != -1){
      bufferedOutputStream.write(buffer, 0, len);
      }
      } catch (IOException e) {
      e.printStackTrace();
      } finally {
      // 4.资源关闭
      // 要求:先关闭外层的流,再关闭内层的流
      if (bufferedOutputStream != null) {
      try {
      bufferedOutputStream.close();
      } catch (IOException e) {
      e.printStackTrace();
      }
      }
      if (bufferedInputStream != null) {
      try {
      bufferedInputStream.close();
      } catch (IOException e) {
      e.printStackTrace();
      }
      }
      // 说明:在关闭外层流的同时,内层流也会自动进行关闭,对于内层流的关闭,可以省略
      // fileOutputStream.close();
      // fileInputStream.close();
      }
      }

###### 使用 BufferedReader 和 BufferedWriter

处理文本文件

```java
/**
 * 使用 BufferedReader 和 BufferedWriter 实现文本文件的复制
 */
@Test
public void testBufferedReaderBufferedWriter(){
    BufferedReader bufferedReader = null;
    BufferedWriter bufferedWriter = null;
    try {
        // 创建文件和相应的流
        bufferedReader = new BufferedReader(new FileReader(new File("dbcp.txt")));
        bufferedWriter = new BufferedWriter(new FileWriter(new File("dbcp1.txt")));

        // 读写操作
        // 方式一,使用 char[] 数组
        /*char[] cbuf = new char[1024];
        int len = 0;
        while ((len = bufferedReader.read(cbuf)) != -1){
            bufferedWriter.write(cbuf, 0, len);
        }*/
        // 方式二:使用 String
        String data;
        while ((data = bufferedReader.readLine()) != null){
            // 方法一:
            // data 中不包含换行符
            // bufferedWriter.write(data + "\n");
            // 方法二:
            bufferedWriter.write(data);
            // 提供换行的操作
            bufferedWriter.newLine();
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (bufferedReader != null) {
            try {
                bufferedReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (bufferedWriter != null) {
            try {
                bufferedWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

转换流的使用

转换流涉及到的类

属于字符流

  • InputStreamReader:将一个字节的输入流转换为字符的输入流

    解码:字节、字节数组 --> 字符数组、字符串

  • OutputStreamWriter:将一个字符的输出流转换为字节的输出流

    编码:字符数组、字符串 --> 字节、字节数组

说明:编码决定了解码的方式

作用

提供字节流与字符流之间的转换

图示

[外链图片转存中…(img-VVvy7P8O-1638542599462)]

典型实现
@Test
public void test1(){
    InputStreamReader inputStreamReader = null;
    try {
        FileInputStream fileInputStream = new FileInputStream("dbcp.txt");
        // 使用系统默认的字符集
        // InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);
        // 参数2指明了字符集:具体使用哪个字符集,取决于文件 dbcp.txt 保存时使用的字符集
        inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
        char[] cbuf = new char[20];
        int len = 0;
        while ((len = inputStreamReader.read(cbuf)) != -1){
            System.out.print(new String(cbuf,0, len));
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (inputStreamReader != null) {
            try {
                inputStreamReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
/**
 * 综合使用 InputStreamReader 和 OutputStreamWriter
 */
@Test
public void test2(){
    InputStreamReader inputStreamReader = null;
    OutputStreamWriter outputStreamWriter = null;
    try {
        File file1 = new File("dbcp.txt");
        File file2 = new File("dbcp_gbk.txt");

        FileInputStream fileInputStream = new FileInputStream(file1);
        FileOutputStream fileOutputStream = new FileOutputStream(file2);

        inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
        outputStreamWriter = new OutputStreamWriter(fileOutputStream, "gbk");

        char[] cbuf = new char[20];
        int len = 0;
        while ((len = inputStreamReader.read(cbuf)) != -1){
            outputStreamWriter.write(cbuf, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }finally {
        if (inputStreamReader != null) {
            try {
                inputStreamReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (outputStreamWriter != null) {
            try {
                outputStreamWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
说明

文件编码的方式,决定了解析时使用的字符集

编码表
常见编码表
  • ASCII:美国标准信息交换码,用一个字节的7位可以表示。
  • ISO8859-1:拉丁码表。欧洲码表,用一个字节的8位表示。
  • GB2312:中国的中文编码表。最多两个字节编码所有字符
  • GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
  • Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示。
  • UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。
启示

客户端 / 游览器端 <—> 后台(Java, Go, Python, Node.js, PHP) <—> 数据库

要求前前后后使用的字符集都要统一:UTF-8

其它的流的使用

标准的输入输出流
  • System.in:标准的输入流,默认从键盘输入
  • System.out:标准的输出流,默认从控制台输出

修改默认的输入和输出行为:System 类的 setIn(InputStream InputStream) / setOut(OutputStream outputStream) 方式重新指定输入和输出的流

打印流
  • PrintStream
  • PrintWriter

说明

  • 提供了一系列重载的 print() 和 println() 方法,用于多种数据类型的输出
  • System.out 返回的是 PrintStream 的实例
数据流
  • DataInputStream
  • DataOutputStream

作用:用于读取或写出基本数据类型的变量或字符串

示例代码

/**
 * 数据流:DataInputStream 和 DataOutputStream
 * 1.作用:用于读取或写出基本数据类型的变量或字符串
 */
@Test
public void test3(){
    DataOutputStream dataOutputStream = null;
    try {
        dataOutputStream = new DataOutputStream(new FileOutputStream("data.txt"));
        dataOutputStream.writeUTF("刘建辰");
        dataOutputStream.writeInt(23);
        dataOutputStream.writeBoolean(true);
        // 刷新操作,将内存中的数据写入文件
        dataOutputStream.flush();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (dataOutputStream != null) {
            try {
                dataOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
/**
 * 将文件中存储的基本数据类型变量和字符串读取到内存中
 * 注意点:读取不同类型数据的顺序要以当初写入文件时,保存的数据的顺序一致
 */
@Test
public void test4(){
    DataInputStream dataInputStream = null;
    try {
        dataInputStream = new DataInputStream(new FileInputStream("data.txt"));
        System.out.println(dataInputStream.readUTF());
        System.out.println(dataInputStream.readInt());
        System.out.println(dataInputStream.readBoolean());
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (dataInputStream != null) {
            try {
                dataInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

对象流的使用

RandomAccessFile 的使用

Path、Paths、Files 的使用

项目

项目要求

  • 至少独立完成一遍以上的项目代码
  • 积累完成项目的过程中常见的 bug 调试
    • “硬”看,必要时添加输出语句
    • Debug
  • 捋顺思路,强化逻辑
  • 对象、数组等内存结构的解析
  • 遵守编码的规范,标识符的命名规范等
  • 在类前,方法前,方法内具体逻辑的实现步骤等添加必要的注释
    • 类前,方法前,属性前:文档注释
    • 逻辑步骤:单行、多行注释
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-12-04 13:15:32  更:2021-12-04 13:19:56 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 3:20:30-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码