一、java语言概述

1、软件开发介绍

软件,即一系列按照特定顺序组织的计算机数据和指令的集合。有系统软件和应用软件之分。 常用的dos命令

1
2
3
4
5
6
7
8
9
dir : 列出当前目录下的文件以及文件夹
md : 创建目录
rd : 删除目录
cd : 进入指定目录
cd.. : 退回到上一级目录
cd\: 退回到根目录
del : 删除文件
exit : 退出 dos 命令行
补充:echo javase>1.doc

计算机语言:人与计算机交流的方式。

如果人要与计算机交流,那么就要学习计算机语言。C ,C++ ,Java,.net ,PHP , Kotlin,Python,Scala

2、计算机编程语言介绍

第一代语言

机器语言。指令以二进制代码形式存在。

第二代语言

汇编语言。使用助记符表示一条机器指令

第三代语言:高级语言

C、Pascal、Fortran面向过程的语言

C++面向过程/面向对象 Java跨平台的纯面向对象的语言

.NET Python、Scala

3、java语言概述

是SUN(Stanford University Network,斯坦福大学网络公司 ) 1995年推出的一门高级编程语言。是一 种面向Internet的编程语言。Java一开始富有吸引力是因为Java程序可以在Web浏览器中运行。这些Java 程序被称为Java小程序(applet)。applet使用现代的图形用户界面与Web用户进行交互。 applet内嵌 在HTML代码中。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。

1991年 Green项目,开发语言最初命名为Oak (橡树)

1994年,开发组意识到Oak 非常适合于互联网

1996年,发布JDK 1.0,约8.3万个网页应用Java技术来制作

1997年,发布JDK 1.1,JavaOne会议召开,创当时全球同类会议规模之最

998年,发布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

4、java技术平台

image-20220106165903244

5、java使用领域

企业级应用:主要指复杂的大企业的软件系统、各种类型的网站。Java的安全机制以及它的跨平台的优 势,使它在分布式系统领域开发中有广泛应用。应用领域包括金融、电信、交通、电子商务等。 移动领域应用:主要表现在消费和嵌入式领域,是指在各种小型设备上的应用,包括手机、PDA、机顶 盒、汽车通信设备等。

诞生:

image-20220106165944094

java之父James Gosling团队在开发”Green”项目时,发现C缺少垃圾回收系统,还有可移植的安全性、分 布程序设计和多线程功能。最后,他们想要一种易于移植到各种设备上的平台。java是一个纯粹面向对 象的语言。舍弃了c语言中容易引起错误的指针,增加了垃圾回收器,泛型,枚举等功能。

6、java主要特征

Java语言是易学的。Java语言的语法与C语言和C++语言很接近,使得大多数程序员很容易学习和使用 Java。

Java语言是强制面向对象的。Java语言提供类、接口和继承等原语,为了简单起见,只支持类之间的单 继承,但支持接口之间的多继承并支持类与接口之间的实现机制(关键字为implements)。

Java语言是分布式的。Java语言支持Internet应用的开发,在基本的Java应用编程接口中有一个网络应用 编程接口(java net),它提供了用于网络应用编程的类库,包括URL、URLConnection、Socket、 ServerSocket等。Java的RMI(远程方法激活)机制也是开发分布式应用的重要手段。

Java语言是健壮的。Java的强类型机制、异常处理、垃圾的自动收集等是Java程序健壮性的重要保证。 对指针的丢弃是Java的明智选择。

Java语言是安全的。Java通常被用在网络环境中,为此,Java提供了一个安全机制以防恶意代码的攻 击。如:安全防范机制(类ClassLoader),如分配不同的名字空间以防替代本地的同名类、字节代码 检查。

Java语言是体系结构中立的。Java程序(后缀为java的文件)在Java平台上被编译为体系结构中立的字节 码格式(后缀为class的文件),然后可以在实现这个Java平台的任何系统中运行。

Java语言是解释型的。如前所述,Java程序在Java平台上被编译为字节码格式,然后可以在实现这个Java 平台的任何系统的解释器中运行。

Java是性能略高的。与那些解释型的高级脚本语言相比,Java的性能还是较优的。

Java语言是原生支持多线程的。在Java语言中,线程是一种特殊的对象,它必须由Thread类或其子 (孙)类来创建。

7、java程序运行机制与运行过程

Java语言的特点: 1)面向对象:两个基本概念:类和对象。三大特征:封装,继承,多态

​ 2)健壮:吸收了C/C++语言的优点,但去掉了其影响程序健壮性的部分(如指针、内 存的申请与释放等),提供了一个相对安全的内存管理和访问机制

​ 3)跨平台:跨平台性:通过Java语言编写的应用程序在不同的系统平台上都可以运 行。Write once , Run Anywhere。原理:在操作系统上按照java虚拟机(JVM java Virtual Machine). 由JVM来负责Java程序在该系统中的运行。

image-20220106170229323

​ 核心机制:JAVA虚拟机(JAVA Virtal Machine) 垃圾收集机制(Garbage Collection)

​ java虚拟机 :JVM 是一个虚拟的计算机,具有指令集并使用不同的存储区域。负责执行指令,管 理数据,内存,寄存器。对于不同的平台,有不同的虚拟机。只有操作系统是航有对应的java虚拟机, java程序才可在运行。java虚拟机机制屏蔽了底层运行平台的差别,实现了一次编译,到处运行。

image-20220106170409628

​ 垃圾回收:不再使用的内存空间应回收。Java 语言消除了程序员回收无用内存空间的责任:它 提供一种系统级线程跟踪存储空间的分配情况。并在JVM空闲时,检查并释放那些可被释放的存储空 间。垃圾回收在Java程序运行过程中自动进行,程序员无法精确控制和干预。

8、java语言环境搭建

下载JDK,安装JDK,配置环境变量。

验证是否成功

什么是JDK,JRE?

image-20220106170500936

JDK=JRE+开发工具集 JRE=JVM+JAVASE标准类库

安装:傻瓜式安装,默认下一步即可。安装路径不能包含中文,空格等特殊字符。推荐再要安装的路径 下,创建好文件夹。

image-20220106170527913\

image-20220106170541021

image-20220106170549286

image-20220106170601891

image-20220106170610266

image-20220106170623344

image-20220106170630923

image-20220106170640512

image-20220106170647950

image-20220106170656498

配置环境变量

image-20220106170749177

image-20220106170757853

image-20220106170807379

image-20220106170848229

配置path

image-20220106170906910

image-20220106170919329

path:%JAVA_HOME%:取到配置JAVA_HOME环境变量的值

检测是否成功

image-20220106170948280

image-20220106170955140

image-20220106171003495

9、helloworld

将 Java 代码编写到扩展名为 .java 的文件中。

通过 javac 命令对该 java 文件进行编译

通过 java 命令对生成的 class 文件进行运行

image-20220106171133079

image-20220106171140302

image-20220106171147836

image-20220106171201631

​ 编写第一个java程序:

1
2
3
4
5
6
public class Test{
public static void main(String[] args) {
System.out.println(“Hello World!”);
}
}

编译:

1
javac Test.java

有了java源文件,通过编译器将其编译成JVM可以识别的字节码文件。

在该源文件目录下,通过javac编译工具对Test.java文件进行编译。

如果程序没有错误,没有任何提示,但在当前目录下会出现一个Test.class文件,该文件称为字节码文 件,也是可以执行的java的程序。

运行:

1
java Test

有了可执行的java程序(Test.class字节码文件)

通过运行工具java.exe对字节码文件进行执行。出现提示:缺少一个名称为main的方法

因为一个程序的执行需要一个起始点或者入口,所以在Test类中的加入public static void main(String[] args){ }

对修改后的Test.java源文件需要重新编译,生成新的class文件后,再进行执行。

发现没有编译失败,但也没有任何效果,因为并没有告诉JVM要帮我们做什么事情,也就是没有可以具 体执行的语句。

想要和JVM来个互动,只要在main方法中加入一句System.out.println(“Hello World”);因为程序进行改 动,所以再重新编译,运行即可

现在文件的扩展名

image-20220106171454564

10、常见问题及解决办法

image-20220106171532462

image-20220106171539332

image-20220106171549885

源文件名不存在或者写错 当前路径错误 后缀名隐藏问题

image-20220106171834797

类文件名写错,尤其文件名与类名不一致时,要小心类文件不在当前路径下,或者不在classpath指定 路径下

image-20220106171843436

声明为public的类应与文件名一致,否知编译失败

image-20220106171852440

编译失败,注意错误出现的行数,再到源代码中指定位置改错

学习编程最容易犯的错是语法错误。Java要求你必须按照语法规则编写代码。

如果你的程序违反了语法规则,例如:忘记了分号、大括号、引号,或者拼错了单词,java编译器都会 报语法错误。尝试着去看懂编译器会报告的错误信息

image-20220106171941060

或使用转义字符实现换行

image-20220106171956088

11、注释

单行注释 // 多行注释 /**/

文档注释 (java特有)

image-20220106172027886

image-20220106172040733

小结程序

image-20220106172057909

12、良好的编程习惯

image-20220106172125033

练习题:

1、 java语言的特点是什么?

image-20220106172142904

2、System.out.println()与System.out.print()区别是什么?

3、一个.java源文件中是否可以包括多个类?什么要求?

4、Test类的文件名叫Demo.java

5、设置path的目的是什么?

6、JDK JRE 和 JVM的关系是什么?

7、源文件名是否必须和类名相同呢?如果不是,什么情况下相同

8、程序中若只有一个public修饰的类,且此类包含main方法,那么类名和源文件名可否不一致?

9、java注释方式有哪几种,格式是什么?

10、GC是什么?为什么要有GC

image-20220106172220977

11、垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行 垃圾回收

image-20220106172242190

12、输出以下内容

image-20220106172300143

二、数据类型变量与运算符

1、数据类型

​ 在生活中,使用的数据有大有小,有整数,也有小数,同时也会使用字母,或者汉字表示特定含 义。在Java中根据数据的不同特点,数据也分为不同的类型。Java语言是强类型语言,对于每一种数据 都定义了明确的具体数据类型,在内存总分配了不同大小的内存空间

image-20220106172337347

image-20220110093928701

注意:整数默认int,小数默认double。boolean类型的值只有true false

2、标识符

自己起名字的地方都叫标识符。

​ Java中的名称命名规范:可以包含数字,字母,_,$.但是开头不能是数字,不能是java中的关键字。 命名是区分大小写的。

包名:多单词组成时所有字母都小写:xxxyyyzzz

类名、接口名:多单词组成时,所有单词的首字母大写:XxxYyyZzz。帕斯卡命名规则

变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写: xxxYyyZzz。驼峰命名规则

常量名:所有字母都大写。多单词时每个单词用下划线连接:XXX_YYY_ZZZ

注意1:在起名字时,为了提高阅读性,要尽量有意义,“见名知意”。

注意2:java采用unicode字符集,因此标识符也可以使用汉字声明,但是不建议使用

3、关键字与保留字

关键字(keyword)的定义和特点

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

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

image-20220106172856220

字面值: true false null

保留字:现有Java版本尚未使用,但以后版本可能会作为关键字使用。自己命名标识符时要避免使用 这些保留字:goto const

4、变量

​ 1> 概念:内存中的一个存储区域。该区域的数据可以在同一个类型范围内不断变化。变量是程序中 最基本的存储单元。包含:变量类型 变量名 值;

​ 作用:用于在内存中保存数据的。

​ 注意:先声明再使用

​ 使用变量名来访问这块区域的数据

​ 变量的作用域:其定义所在的一对{ }内

​ 变量只有在其作用域内才有效

​ 同一个作用域内,不能定义重名的变量

​ 2>使用

​ 声明变量: 数据类型 变量名;

​ 赋值: 变量名=值;

​ 合并以上步骤:数据类型 变量名 = 值;

​ 变量除去按照数据类型来分后,还可以按照位置来分【了解,后面详解】

image-20220106173207732

​ 3> 整形变量:byte short int long

Java各整数类型有固定的表数范围和字段长度,不受具体OS的影响,以保证java程序的可移植性。

java的整型常量默认为 int 型,声明long型常量须后加‘l’或‘L’

java程序中变量通常声明为int型,除非不足以表示较大的数,才使用long

image-20220107140601383

​ 4> 浮点类型:float、double

与整数类型类似,Java 浮点类型也有固定的表数范围和字段长度,不受具体操系统的影响。 浮点型常量有两种表示形式:

十进制数形式:如:5.12 512.0f .512 (必须有小数点)

科学计数法形式:如:5.12e2 512E2 100E-2

float:单精度,尾数可以精确到7位有效数字。很多情况下,精度很难满足需求。

double:双精度,精度是float的两倍。通常采用此类型。

Java 的浮点型常量默认为double型,声明float型常量,须后加f或F。

image-20220106173404278

​ 5>字符类型 char

char 型数据用来表示通常意义上“字符”(2字节)

Java中的所有字符都使用Unicode编码,故一个字符可以存储一个字母,一个汉字,或其他书面语的一 个字符。

字符型变量的三种表现形式:

字符常量是用单引号(‘ ’)括起来的单个字符。例如:char c1 = ‘a’; char c2 = ‘中’; char c3 = ‘9’;

Java中还允许使用转义字符‘\’来将其后的字符转变为特殊字符型常量。

例如:char c3 = ‘\n’; // ‘\n’表示换行符

直接使用 Unicode 值来表示字符型常量:‘\uXXXX’。其中,XXXX代表 一个十六进制整数。如:\u000a 表示 \n。

char类型是可以进行运算的。因为它都对应有Unicode码。

扩展:

ascii 码在计算机内部,所有数据都使用二进制表示。每一个二进制位(bit)有 0 和 1 两种状 态,因此 8 个二进制位就可以组合出 256 种状态,这被称为一个字节(byte)。一个字节一共可以用来 表示 256 种不同的状态,每一个状态对应一个符号,就是 256 个符号,从0000000 到 11111111。

ASCII码:上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规 定。这被称为ASCII码。ASCII码一共规定了128个字符的编码,比如空格“SPACE”是32(二进制 00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制 符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。

ASCII编码表中,A=65,a=97

缺点:不能表示所有字符。 相同的编码表示的字符不一样。

unicode码:乱码:世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此, 要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。 Unicode:一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,使用 Unicode 没有乱码的问题。

Unicode 的缺点:Unicode 只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储:无 法区别 Unicode 和 ASCII:计算机无法区分三个字节表示一个符号还是分别表示三个符号。另外,我们 知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示, 那么每个英文字母前都必然有二到三个字节是0,这对于存储空间来说是极大的浪费。

​ 6>boolean类型:boolean类型数据只允许取值true和false,无null。

不可以使用0或非 0 的整数替代false和true,这点和C语言不同。

Java虚拟机中没有任何供boolean值专用的字节码指令,Java语言表达所操作的boolean值,在编译之后 都使用java虚拟机中的int数据类型来代替:true用1表示,false用0表示。

5、类型转换

​ 1>自动类型转换

容量小的类型自动转换为容量大的数据类型。数据类型按容量大小排序为

image-20220106173920283

​ 有多种类型的数据混合运算时,系统首先自动将所有数据转换成容量最大的那种数据类型,然后再进 行计算。

byte,short,char之间不会相互转换,他们三者在计算时首先转换为int类型。

boolean类型不能与其它数据类型运算

当把任何基本数据类型的值和字符串(String)进行连接运算时(+),基本数据类型的值将自动转化为字符 串(String)类型

​ 2>强制类型转换

自动类型转换的逆过程,将容量大的数据类型转换为容量小的数据类型。使用时要加上强制转换符: (),但可能造成精度降低或溢出,格外要注意 。

通常,字符串不能直接转换为基本类型,但通过基本类型对应的包装类则可以实现把字符串转换成基本 类型

boolean类型不可以转换为其他的数据类型

扩展:进制 所有数字在计算机底层都以二进制形式存在

对于整数,有四种表示方式:

二进制(binary):0,1 ,满2进1.以0b或0B开头。

​ 二进制的整数有如下三种形式:

​ 原码:直接将一个数值换成二进制数。最高位是符号位

​ 负数的反码:是对原码按位取反,只是最高位(符号位)确定为1。

​ 负数的补码:其反码加1。

​ 计算机以二进制补码的形式保存所有的整数。

​ 正数的原码、反码、补码都相同

​ 负数的补码是其反码+1

image-20220106173933617

十进制(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

image-20220106173954580

6、 运算符

运算符是一种特殊的符号,用以表示数据的运算、赋值和比较等。

image-20220106174011615

课堂讲解:

image-20220106174025758

练习:

1、在内存中存储本金1000元 显示内存中存储的数据的值

2、 使用变量存储数据,实现个人简历信息的输出image-20220106174038013

3、 输出Java课考试最高分:98.5 输出最高分学员姓名:张三 输出最高分学员性别:男

4、从控制台输入学员王浩3门课程成绩,编写程序实现 Java课和SQL课的分数之差 3门课的平均分

image-20220106174050004

5、 根据天数(46)计算周数和剩余的天数

6、已知圆的半径radius= 1.5,求其面积

7、键盘输入四位数字的会员卡号 使用“/”和“%”运算符分解获得会员卡各个位上的数字 将各个位上数字 求和

8、 根据分解后的数字之和,判断用户是否中奖image-20220106174117471

9、 从控制台输入张三同学的成绩,与李四的成绩(80分)比较,输出“张三的成绩比李四的成绩高吗?” 的判断结果

10、打印购物小票

image-20220106174128902

11、 为抵抗洪水,解放军战士连续作战89小时,编程计算共多少天零多少小时?

12、 自定义一个整数,输出该数分别与1~10相乘的结果。

13、思考,以下代码有区别吗?image-20220106174139732

14、对两个数实行等量交换

image-20220106174151200

image-20220108155136069

三、流程控制语句

流程控制语句是用来控制程序中各语句执行顺序的语句,可以把语句组合成能完成一定功能的小逻辑模 块。

其流程控制方式采用结构化程序设计中规定的三种基本流程结构,即:顺序结构,分支结构,循环结 构。

顺序结构:程序从上到下逐行执行,中间没任何判断和跳转。

分支结构:选择结构。有两类,一类是if,一类是switch

循环结构:while,do…while,for循环。

1、选择结构

1.if语句

有三种格式:

image-20220108152638682

image-20220108152650310

image-20220108152659491

2、switch语句

image-20220108152728036

switch(表达式)中表达式的值必须是下述几种类型之一:byte short char,int,枚举 (jdk 5.0),String (jdk7.0);

case子句中的值必须是常量,不能是变量名或不确定的表达式值;

同一个switch语句,所有case子句中的常量值互不相同;

break语句用来在执行完一个case分支后使程序跳出switch语句块;如果没有break,程序会顺序执行到 switch结尾

default子句是可任选的。同时,位置也是灵活的。当没有匹配的case时,执行default。

练习1 :

​ 1、如果成绩在90分以上是A,80分以上是B,70分以上是C,60分以上是D,其他 E

​ 2、今天是周末还是工作?如果是周末,如果气温大于36度,就游泳,否则就爬山,如果是工作日,看看天气好嘛?如果天气好,就去拜访客户,如果天气不好,就在单位查资料。

​ 3、飞机票的原价是 5000元,12-3旺季 旺季头等舱打折扣9折,经济舱是8折 其余时间是淡季, 头等舱,打5折,经济舱打四折用户输入出行的月份,选择头等舱还是经济舱,请输出用户应支付的机票 的价格 请输入出行的月份: 3 请选择 1.头等舱 2.经济舱 您的机票的价格为:4500元

​ 4、如果考试考了第一名,奖励一个笔记本,如果第二名,奖励一个移动硬盘,如果第三名,奖励了一个U盘,否则没任何奖励。

练习2 :

​ 1、输入今天星期几?一三五,吃鱼。二四六吃肉。日吃素

​ 2、商场搞活动,换购活动,1. 满50元,可以加两元换购一瓶矿泉水,2.满100元,可以加五元换 购一瓶矿泉水,3.满100元,加10元换购炒锅一个,4.满200元,可以加5元换购一瓶爽肤水,5.满200 元,加10元换购保温壶一个 0不换购。

​ 3、输入三个数,然后按照从小到大顺序输出三个数。

​ 4、输入一个四位数,如果这个四位数和随机生成的一个数相同,则终奖了

练习3:

image-20220108152936000

image-20220108152948347

image-20220108153044952

3、设计一个迷你计算器

image-20220108153101079

3、从键盘分别输入判断一年是否是闰年的标准:

​ 1)可以被4整除,但不可被100整除或

​ 2)可以被400 整除

4、假设你想开发一个玩彩票的游戏,程序随机地产生一个两位数的彩票,提示用户输入一个两位数,然 后按照下面的规则判定用户是否能赢。

​ 1)如果用户输入的数匹配彩票的实际顺序,奖金10 000美元。

​ 2)如果用户输入的所有数字匹配彩票的所有数字,但顺序不一致,奖金 3 000美元。

​ 3)如果用户输入的一个数字仅满足顺序情况下匹配彩票的一个数字,奖金1 000美元。

​ 4)如果用户输入的一个数字仅满足非顺序情况下匹配彩票的一个数字,奖金500美元。

​ 5)如果用户输入的数字没有匹配任何一个数字,则彩票作废。

提示:使用Math.random() 产生随机数

Math.random() 产生[0,1)范围的随机值

Math.random() * 90:[0,90)

Math.random() * 90 + 10:[10,100) 即得到 [10,99]

使用(int)(Math.random() * 90 + 10)产生一个两位数的随机数。

5、提供三个1-6的随机数,作为掷骰子得到的点数。

​ 如果各个点数相同,则为豹子。

​ 如果三个骰子点数和,小于或等于9,则为“小”。

​ 如果三个骰子点数和,大于9,则为“大”。

​ 用户随机选择:押大、押小或者押豹子。通过判断,输出客户是否押正确。

​ //1、产生一个[1-6]的数 int a = (int)(Math.random()*6+1);

6、编写一个程序,为一个给定的年份找出其对应的中国生肖。中国的生肖基于12年一个周期,每年用 一个动物代表:rat(鼠)、ox(牛)、tiger(虎)、rabbit(兔)、dragon(龙)、snake(蛇)、 horse(马)、sheep(羊)、monkey(候)rooster(鸡)、dog(狗)、pig(猪)。

​ 提示:2019 年:猪 2019 % 12 == 3

7、随机生成一个100以内的数,猜数字游戏:从键盘输入数,如果大了提示,大了,如果小了,提示小 了,如果对了,就不再猜了,并统计一共猜了多少次?提示:随机数

8、一个数如果恰好等于它的因子之和,这个数就称为”完数”。(因子:除去这个数本身的约数)例如 6=1+2+3.编程 找出1000以内的所有完数

9、输入两个正整数m和n,求其最大公约数和最小公倍数

switch与if比较

if和switch语句很像,具体什么场景下,应用哪个语句呢?

如果判断的具体数值不多,而且符合byte、short 、char、int、String、枚举等几种类型。虽然两个语 句都可以使用,建议使用swtich语句。因为效率稍高。 其他情况:对区间判断,对结果为boolean类型 判断,使用if,if的使用范围更广。也就是说,使用switch-case的,都可以改写为if-else。反之不成立。

2、循环结构

在某些条件满足的情况下,反复执行特定代码的功能。有三种循环,分布是while 循环,do-while循环 以及for循环。

循环会有四要素:初始化变量,循环条件,循环操作,改变变量的值防止死循环。

生活中的循环:

image-20220108153437109

1>while循环

image-20220108153451153

image-20220108153501638

特点:先判断再执行;

2>do-while

image-20220108153515759

特点:先执行再判断

while循环和do-while循环的区别

3>for循环

image-20220108153546189

3、跳转语句

break:改变程序控制流

用于do-while、while、for中时,可跳出循环而执行循环后面的语句 用在switch语句中

image-20220108153611270

continue,只能用在循环里面。作用:跳过循环体中剩余的语句而执行下一次循环

image-20220108153634021

return:并非专门用于结束循环的,它的功能是结束一个方法。

当一个方法执行到一个return语句时,这个方法将被结束。

与break和continue不同的是,return直接结束整个方法,不管这个return处于多少层循环之内

练习:

​ 1、打印1-100之间的所有奇数的和

​ 2、打印1~100之间所有是7的倍数的整数的个数及总和(体会设置计数器的思想)

​ 3、输出所有的水仙花数,所谓水仙花数是指一个3位数,其各个位上数字立方和等于其本身

​ 例如:153 = 111 + 333 + 555

​ 4、从键盘读入个数不确定的整数,并判断读入的正数和负数的个数,输入为0时结束程序

​ 5、用户登录系统,最多输入三次用户名与密码,提醒用户登录是否成功,以及还有几次机会。

​ 6、实现循环购物(第二章练习)

image-20220108153731332

     7、 假设一个简单的ATM机的取款过程是这样的:首先提示用户输入密码(password),最多只能输入 三次,超过三次则提示用户“密码错误,请取卡”结束交易。如果用户密码正确,再提示用户输入金额 (amout),ATM机只能输出100元的纸币,一次取钱要求最低0元,最高1000元,如果用户输入的金额符 合上述要求,则打印输出用户取的钱数,最后提示用户“交易完成,请取卡”,否则提示用户重新输入金 额。假设用户密码是111111。
 
     8、 编写JAVA程序,实现输出1--100之间所有不能被7整除的数,并求和要求:每输出4个数据换行显

image-20220108153754770

​ 9、求阶乘

image-20220108153811593

​ 10、实现菜单循环

image-20220108153828521

二重循环:打印九九乘法表

image-20220108153916237

image-20220111165134257

四、数组

1、数组的概述

在执行程序的过程中,经常需要存储大量的数据,例如,假设需要读取100个数,计它们的平均值,然 后找出有多少个数大于平均值。首先,程序读入这些书并且计算它们的平均值,然后将每个数与平均值 进行比较判断它是否大于平均值。为了完成这个任务,必须将全部的数据存储到变量中。必须声明100 个变量,并且重复书写100次几乎完全相同的代码。这样编写程序的方式似乎是不太现实的,那么该如 何解决这个问题呢?

Java和许多高级语言都提供了一种称作数据(array)的数据结构,可以用它来存储一个元素个数固定且 元素类型下相同的有序集。数组主要解决多变量多数据的存储问题,方便程序后期统一维护操作数据。 数组的本质是什么呢?数组就是一系列空间大小相等且地址连续的一片存储空间。为什么空间大小是相 等的呢?就是为了方便统一维护我们的数据,必须得保证数据之间的类型是一样的。为什么变量空间的 地址是连续的呢?地址连续切大小相等方便计算后续元素的具体物理内存地址。

数组就是一片地址连续且空间大小一致的存储空间(但是每个空间存的还是其他数据的地址。数组存在于 堆内存中,但凡在堆中存储的数据都称之为对象。数组提供下标来访问数组当中的元素。数组变量存的 就是数组在堆内存中首元素的地址。数组通过下标来访问元素的具体计算方式是:所要访问数据的地址 = 首元素地址 + 下标 * 数据类型大小。数组一旦定义下来,其长度不可改变;数组中有几个地址?就看 数组有几个元素空间(数组的长度)。创建数组时必须明确规定大小或内容。

2、一维数组的使用

创建数组只指定长度但不指定内容 数据类型[] 数组名 = new 数据类型[长度];

创建数组指定内容(同时长度就确定了)

数据类型[] 数组名 = new 数据类型[]{数据1, 数据2, 数据3, …, 数据n};

数据类型[] 数组名 = {数据1, 数据2, 数据3, …, 数据n}

数组默认值

image-20220108154111832

image-20220108154121749

定义并用运算符new为之分配空间后,才可以引用数组中的每个元素;

数组元素的引用方式:数组名[数组元素下标]

数组元素下标可以是整型常量或整型表达式。如a[3] , b[i] , c[6*i];

数组元素下标从0开始;长度为n的数组合法下标取值范围: 0 —>n-1;如int a[]=new int[3]; 可引用的数 组元素为a[0]、a[1]、a[2]

每个数组都有一个属性length指明它的长度,例如:a.length 指明数组a的长度(元素个数)

数组一旦初始化,其长度是不可变的

3、一维数组内存分析

image-20220108154207485

目前关注:方法区,虚拟机栈,堆

栈stack:局部变量

堆heap:new出来的结构

方法区:method area 静态域 常量池 类的元数据

4、一维数组操作

​ 1> 数组遍历

​ 2>查找数组中的最大值和最小值

image-20220108154415032

​ 3>查找元素在数组中是否存在

image-20220108154431147

​ 4>数组倒着输出

​ 5>数组添加元素,修改元素,以及删除元素

image-20220108154447248

image-20220108154631184

​ 6>数组元素复制

image-20220108154645368

image-20220114095442324

练习:

1>循环录入8个成绩,查找到最大值最小值值,以及其位置

2>输入五个成绩,求总分,以及按照从小到大排序

3>给定一个数列3,4,12,56,45 用户输入要查找的数,判断该数在该数列中是否存在

4>用户输入十个汉字,分别统计 王,李,张的姓氏的个数,其他均认为是非法字符,统计非法字符的 个数

5>有五种水果,apple,pear,banana,orange,mango按英文字母排列 这五种水果

image-20220113144029491

6>输入五句话,然后倒着输出

7>实现如下功能:

image-20220108155235177

8>阅读代码,直接写出结果

image-20220108155248477

9>从键盘读入学生成绩,找出最高分,并输出学生成绩等级。成绩>=最高分-10 等级为’A’ 成绩>=最高 分-20 等级为’B’ 成绩>=最高分-30 等级为’C’ 其余 等级为’D’

10>打印斐波那契数列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55

11>创建要给长度为6的int类型数组,要求数组元素的值再1-30之间,且随机赋值。同时要求数组元 素各不相同

12、数组反转:

image-20220111175351166

13、数组查找【线性查找与二分查找】

二分查找:数组是有序的。

image-20220111175420442

image-20220111175427182

排序算法:十大内部排序算法

选择排序【直接选择排序,堆排序】

交换排序【冒泡排序,快速排序 (手写代码)】

插入排序【直接插入排序,折半插入排序,希尔排序】

归并排序

桶式排序

基数排序

1>冒泡排序

image-20220108155318090

image-20220111175514841

image-20220111175520596

2> 选择排序

image-20220111175531687

image-20220111175539005

image-20220111175544789

3> 插入排序

image-20220111175601483

image-20220111175608424

image-20220111175614819

4> 快速排序【使用递归】

image-20220111175913536

image-20220111175738797

image-20220111175749603

image-20220111175937662

5、二维数组

Java 语言里提供了支持多维数组的语法。如果说可以把一维数组当成几何中的线性图形,那么二维数组 就相当于是一个表格,像右图Excel中的表格一样。对于二维数组的理解,我们可以看成是一维数组 array1又作为另一个一维数组array2的元素而存在。其实,从数组底层的运行机制来看,其实没有多维 数组。

格式1(动态初始化)

image-20220117145305390

定义了名称为arr的二维数组二维数组中有3个一维数组 每一个一维数组中有2个元素一维数组的名称分别为arr[0], arr[1], arr[2]给第一个一维数组1脚标位赋值 为78写法是:arr[0][1] = 78;

image-20220117145332628

格式2(动态初始化):

int[][] arr = new int[3][];二维数组中有3个一维数组。每个一维数组都是默认初 始化值null (注意:区别于格式1)

可以对这个三个一维数组分别进行初始化arr[0] = new int[3]; arr[1] = new int[1]; arr[2] = new int[2];

image-20220111180016545

练习:1.打印杨辉三角image-20220111180207529

image-20220111180217786

五、初步认识初始面向对象

1、面向对象和面向过程

面向过程也是解决问题的一种思想,当我们在解决问题时,会按照预先设定的想法和步骤,一步一步去 实现,而具体的每一步都需要我们去实现和操作。这些步骤相互调用和协作,完成我们的需求。上述描 述的每一个具体步骤我们都是参与者,并且需要面对具体的每一个步骤和过程,这就是面向过程最直接 的体现。通过上面简单的描述发现,面向过程,其实就是面向着具体的每一个步骤和过程,就是面对具 体的每一个功能函数。这些功能函数相互调用,完成需求。

面向对象当不再面对具体的每一个方法时,发现操作也变的简单了很多。而封装具体功能的这类,是我 们需要面对的。而基于这个封装了具体功能的类,那怎么使用呢?当面向封装了具体功能类,若要使用 这个类,一般情况下,在Java中需要通过创建这个类的实体来使用。这个实体称之为对象。在开发中, 我们是在不断的找封装不同功能的类。基于这些类,创建其对象,使用这些对象完成相应的操作。通过 上面的讲解和分析得出:面向对象是基于面向过程,对象是将功能进行了封装。只要找到了具体的类, 创建出对象,就可以调用其中的具体功能。面向对象也是用来解决问题的一种思维模式。在以后开发 中,先找对象,调用对象中的具体功能。如果真的没有能够完成需求的对象,这时就自己创建对象,并 将所需的功能定义到对象中,方便以后使用。

面向对象是一种更符合人们思考习惯的思想面向过程中更多的体现的是执行者,面向对象中更多的体现 是指挥者。指挥对象做事情面向对象将复杂的问题简单化在面向对象的世界中:万事皆对象。

面向对象和面向过程,其实都是一种思想。面向对象是相对面向过程而言的。面向过程,强调的是功能 行为,以函数为最小单位,考虑怎么做。面向对象,将功能封装到对象中,强调具备了功能的对象,以 类/对象为最小的单位,考虑谁来做。

面向对象分析和设计的思路:

根据问题需要,选择问题所针对的现实世界中的实体。

从实体中寻找解决问题相关的属性和功能,这些属性和功能就形成的概念世界中的类。

把抽象的实体用计算机语言进行描述,形成计算机世界中类的定义。即借助某种程序语言,把类构造成 计算机能够识别和处理的数据结构。

把类实例化成计算机世界中的对象,对象是计算机世界中解决问题的最终工具。

2、 类和对象的关系

面向对象程序设计(OOP)就是使用对象进行程序设计。对象(Object)代表现实世界中可以明确标识 的一个实体。例如:一个学生、一张桌子、一个圆、一个按钮甚至是一笔贷款都可以看作是一个对象。 每个对象都有自己独特的标识、状态和行为。一个对象的状态(state,也称为特征(property)或者属 性(arrtibute))是由具有当前值的数据域来表示的。例如:圆对象具有一个数据域radius,它是标识 圆的属性。一个矩形对象具有数据域width和height,它们都是描述矩形的属性。我们一般把对象的特有 属性称之为成员变量。

一个对象的行为(behavior,也称为动作(action))是由方法定义的。调用对象的一个方法就是要求 对象完成一个动作。例如:可以为圆对象定义一个名为getArea()和getPerimeter()的方法。圆对象可以 调用getArea()返回圆的面积,调用getPerimeter()返回它的周长。我们一般把对象的行为称之为成员函 数。

使用一个通用类来定义同一类型的对象。类是一个模板、蓝本或者说是合约,用来定义对象的数据域是 什么以及方法是做什么的。一个对象是类的一个实例。可以从一个类中创建多个实例。创建实例的过程 称为实例化。对象和实例经常是可以互换的。

类是用于描述现实事物的,它将现实事物进行抽象化,模板化描述。将事物的特点(属性)和行为封装 在其中。比如小汽车的图纸,图纸就是小汽车的模版。图纸上画着小汽车的各种特点和功能要求。

Java类使用变量定义数据域,使用方法定义动作。除此之外,类还提供了一种称为构造方法 (constructor)的特殊类型的方法,调用它可以创建一个新对象。构造方法本身是可以完成任何动作 的,但是设计构造方法是为了完成初始化动作。

对象是现实生活中存在的具体的实例、个体。即生活看到每一个事物,以及我们想象中的任务抽象的概 念,都是某一类事物的实例和个体。而这些个体都属于某一类事物,即这些个体都是某一类事物中的具 体的实例。比如,小汽车就是一类事物,而小汽车又是基于小汽车图纸制造出来的真实个体。因此我们 生活中的每一个实物都可以理解为是某一类事物的中的一个个体。创建对象的,通过对象就可以调用具 体的属性和行为。

类和对象是面向对象的核心概念。类是对一类事物的描述,是抽象的概念上的定义。对象是实际存在的 该类事物的每个个体,因而也称为实例。类是抽象的概念,对象是具体的概念。

可以理解:类=抽象概念的人,对象=实实在在的某个人

面向对象程序设计的重点是类的设计

类的设计,其实就是类的成员设计。常见的类的成员有:属性【类中的成员变量】,方法【行为,类中 的成员方法】。

image-20220117145659074

如果将对象比作汽车,那么类就是汽车的设计图纸。所以面向对象程序设计的重点是类的设计,而不是 对象的设计。image-20220117145709494

类是模子,规定对象将会拥有的属性和方法

3、类的语法格式:

image-20220117145732485

java创建类的步骤:

定义类(考虑修饰符,类名)

编写类的属性(考虑修饰符,属性类型,属性名,初始化)

编写类的方法(考虑修饰符,返回值类型,方法名,形参等)。

image-20220117145806016

image-20220117145838771

image-20220117145848947

练习:

1、 创建一个汽车类,包含属性有 品牌 型号 价格 颜色 使用年限。打印输出车的信息

2、创建一个人类,包含人的姓名,性别,年龄。包含的吃饭的方法,睡觉的方法 编写好类后,如何使用?需要创建类的对象以及调用属性和方法

3、创建管理员类,包含属性 用户名和密码,有一个输出方法,打印输出管理员信息。[类的设 计]

[实现修改管理员密码,要求先登录,如果登录成功,有权限修改密码,否则不能修改 main] 。

image-20220117145926714

匿名对象:我们也可以不定义对象的引用,而直接调用这个对象的方法。这样的对象叫做匿名对象, 如:new Person().shout(); 如果对一个对象只需要进行一次方法调用,那么就可以使用匿名对象 。

4、编写学生类和教师类,并通过测试类创建对象进行测试

image-20220117150002147

4、成员变量

成员变量又访问修饰符【后面详解】

可以是任意类型

类中定义的变量称为成员变量,在方法中定义的变量是局部变量。成员变量和局部变量的区别。

image-20220117150129394

当一个对象被创建时,会对其中各种类型的成员变量自动进行初始化赋值。除了基本数据类型之外的变 量类型都是引用类型

image-20220117150151420

image-20220117150159549

image-20220117150213129

5、方法

方法是类或对象行为特征的抽象,用来完成某个功能操作,在某些语言中也称为函数或过程。

将功能封装为方法的目的是,实现代码重用,简化代码。

java中的方法不独立存在,所有的方法定义在类中。

案例:榨汁机案例【举例】

类的方法定义类的某种行为(或功能)

image-20220117150253394

方法分类: 按照有无返回值分类 :1> 无返回值 void 2>有返回值 方法体最后加return。

方法调用:方法是个黑匣子,完成某个特定的应用程序功能,并返回结果。方法通过方法名被调用。只有 被调用才会执行。

调用方法就是执行方法中包含的语句。方法之间允许相互调用,不需要知道方法的具体实现,实现 宠重用,提高效率。

方法之间存在相互调用,方法内部不能重复定义方法,只能调用另一个方法。

image-20220117150334416

方法重载:方法名相同,参数列表不同,与访问修饰符与返回值类型无关,形成方法的重载。

如果一个方法调用自身,我们称为方法的递归调用。递归容易出现堆栈溢出,必须找到出口,结束 该方法的执行。

方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制。

递归一定要向已知方向递归,否则这种递归就变成了无穷递归,类似于死循环。

image-20220117150425780

6、对象的创建和使用:内存解析

image-20220117150442689

堆(Heap),此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一 点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。

通常所说的栈(Stack),是指虚拟机栈。虚拟机栈用于存储局部变量等。局部变量表存放了编译期可知 长度的各种基本数据类型(boolean、byte、char 、 short 、 int 、 float 、 long 、double)、对象引 用(reference类型,它不等同于对象本身,是对象在堆内存的首地址)。 方法执行完,自动释放。

方法区(Method Area),用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的 代码等数据。

对象内存分析【初步】

image-20220117150516357

练习:

1、创建客户类,有客户类型以及积分。现在回馈客户,如果是金卡积分在1000以上,或者是普 卡,积分在5000以上,可以获赠500积分。

2、编写游客类,包括客户名和年龄,输出客户的门票价格,如果在12岁以下或60岁以上,门票 不要钱,否则门票打五折。【门票原价100元】

3、编写一个Student类,包含name、gender、age、id、score属性,分别为String、String、 int、int、double类型。类中声明一个say方法,返回String类型,方法返回信息中包含所有属性值。

4、创建Person类,有属性name,age,sex属性,调用study方法,输出客户信息,调用person的 showAage的方法,给age属性加1。

5、利用面向对象的思想的编程方法,设计及时circle的圆面积和圆周长

6、创建银行客户类Account类,有卡号cid,余额 balance,密码password.有方法,返回卡号详 细信息,密码显示6个*。设计取钱和存钱的方法。该银行卡号是从属某个Customer类。其中Customer 类有姓名,身份证号,联系电话,家庭地址等属性,有say方法,可以输出个人信息。

7、对象数组

数组的类型是自己定义的类的类型,也就是一组相关对象的集合。利用对象数组完成对学生信息的查 看,添加,修改,删除。

练习:

1、创建学术类,包含学号,姓名,年龄,成绩,创建5个学生,成绩右随机数生成。打印学生信 息,并按照成绩由小到大的顺序输出。

2、设计计算器的类,可以实现两个数相加,三个数详解,四个数相加,若干个数详解

8、动态参数

JavaSE 5.0 中提供了Varargs(variable number of arguments)机制,允许直接定义能和多个实参相匹 配的形参。从而,可以用一种更简单的方式,来传递个数可变的实参。

  1. 声明格式:方法名(参数的类型名 …参数名)

  2. 可变参数:方法参数部分指定类型的参数个数是可变多个:0个,1个或多个

  3. 可变个数形参的方法与同名的方法之间,彼此构成重载

  4. 可变参数方法的使用与方法参数部分使用数组是一致的

  5. 方法的参数部分有可变形参,需要放在形参声明的最后

  6. 在一个方法的形参位置,最多只能声明一个可变个数形参

9、值传递和引用传递

image-20220117150938568

形参是基本数据类型:将实参基本数据类型变量的“数据值”传递给形参

形参是引用数据类型:将实参引用数据类型变量的“地址值”传递给形参

image-20220117151004539

image-20220117151022390

image-20220117151031703

练习:

1、什么是方法重载

2、java中参数传递的方式

3、成员变量和局部变量的区别

4、说说你对方法的理解

六、封装

1、封装

​ 我们程序设计追求“高内聚,低耦合”。高内聚 :类的内部数据操作细节自己完成,不允许外部干 涉;低耦合 :仅对外暴露少量的方法用于使用。

​ 隐藏对象内部的复杂性,只对外公开简单的接口。便于外界调用,从而提高系统的可扩展性、可维 护性。通俗的说,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。image-20220117151210949

面向对象特点–抽象 从许多事物中舍弃个别的、非本质的特征,抽取共同的、本质性的特征,就叫作抽 象。 抽象是形成类的必须手段。 面向对象就是对现实世界的一种抽象 使用面向对象的思想描述以上图 片

Java中通过将数据声明为私有的(private),再提供公共的(public)方法:getXxx()和setXxx()实现 对该属性的操作,以实现下述目的:

  • 隐藏一个类中不需要对外提供的实现细节;

  • 使用者只能通过事先定制好的方法来访问数据,可以方便地加入控制逻辑,限制对属性的不合理操 作;

  • 便于修改,增强代码的可维护性;

  • 提高了代码的重用性 隐藏了实现细节,对外提供访问的方式。

如在以前的代码中,将属性直接定义在类中,用户在创建对象的时候,可以给属性任意赋值,如 age,用户在创建对象的时候,赋值为-20,该值就是一个非法的值。此时可以使用封装。

封装的步骤:

​ 1)属性private

​ 2)提供get/set方法

​ 3)在方法中加入流程控制语句

封装特点:

  • 隐藏一个类的实现细节;
  • 调用者只能通过事先定制好的方法来访问数据,可以方便地加入控制逻辑,限制对属性的不合理操 作;
  • 便于修改,增强代码的可维护性;

四种访问修饰符:

image-20220117151447781

对于class的权限修饰只可以用public和default(缺省)。 public类可以在任意地方被访问。 default 类只可以被同一个包内部的类访问

练习:

1、创建一个学生类,设置学生的成绩在0-100之间

2、创建一个人类,人类的年龄在0-130之间

2、构造方法

在开发中经常需要在创建对象的同时明确对象的属性值,比如员工入职公司就要明确他的姓名、年 龄等属性信息。创建对象就要明确属性值,那怎么解决呢?也就是在创建对象的时候就要做的事情,当 使用new关键字创建对象时,怎么给对象的属性初始化值呢?这就要学习Java另外一门小技术,构造函 数。那什么是构造函数呢?从字面上理解即为构建创造时用的函数,即就是对象创建时要执行的函数。 既然是对象创建时要执行的函数,那么只要在new对象时,知道其执行的构造函数是什么,就可以在执 行这个函数的时候给对象进行属性赋值

构造方法特征:名字和类名保持一致,没有返回值类型 不能被static,final,等修饰,不能有return 语句

构造方法的作用:创建对象,对对象进行初始化。 带参构造方法可以给属性赋值。

语法格式:修饰符 类名(参数列表){

​ 初始化语句;

​ }

隐式无参构造方法(系统默认提供)

显示定义一个或多个构造方法(无参,有参)

注意:

  • Java语言中,每个类都至少有一个构造方法

  • 一个类中如果没有定义构造方法,默认的有一个public修饰的无参的构造方法。

  • 在一个类中可以定义多个构造方法,形成方法的重载。

  • 如果在类中定义构造方法后,默认的无参构造方法就不存在了。

  • 构造方法不能被继承

JavaBean:

  • 是一种java语言写成的可重用组件

  • 所谓javaBean是指复合如下标准的java

  • 类 类是公共的,public修饰

  • 有一个无参的默认构造方法

  • 属性私有化,提供相应的get|set方法

3、this关键字

this:代表当前对象

可以通过this调用属性,方法,以及构造方法。

通过this调用构造方法,必须放在语句的第一行。

在java中,this关键字是比较难理解的,他在方法内部使用,即这个方法所属对象的应用。在 构造方法总使用,表示该构造方法正在初始化的对象。

如果成员变量和局部变量同名的时候,可以使用this来区分。

总结:

  • 可以在类的构造器中使用”this(形参列表)”的方式,调用本类中重载的其他的构造器!

  • 明确:构造器中不能通过”this(形参列表)”的方式调用自身构造器

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

  • “this(形参列表)”必须声明在类的构造器的首行!

  • 在类的一个构造器中,最多只能声明一个”this(形参列表)”

4、 包与import

package语句作为Java源文件的第一条语句,指明该文件中定义的类所在的包。(若缺省该语句, 则指定为无名包)。它的格式为:package xxx.xxx.xxx;

包对应于文件系统中的目录,package语句中,用”.”来指明包(目录)的层次。

包通常用小写单词标识,通常使用所在公司网址的倒置。com.openlab

作用:

  • 包帮助管理大型,将功能相近的类放到一个包中
  • 便于管理类
  • 解决类命名冲突问题
  • 控制访问权限

使用import导入不同的包下的类

练习:

1、编写一个类Teacher,代表教员,要求如下:a) 具有属性:姓名,年龄,其中年龄不能 小于26岁,否则输出错误信息b) 具有方法:自我介绍,负责输出该教员的年龄,姓名c) 编写测试类 TeacherTest进行测试,看是否符合要求。

2、编写一个类Teacher2,代表教员,要求如下a) 具有属性:姓名,年龄,性别和专业 b) 具有方法:自我介绍,负责输出该教员的年龄,姓名,性别和专业c) 具有两个带参的构造方法,第一个 构造方法中,设置教员的性别为男,专业为Java,其余属性的值由参数给定,第二个构造方法中,所有 属性的值都由参数给定d) 编写测试类Teacher2Test,分别以两种方式完成对两个Teacher2对象的初始 化工作,并分别调用它们的自我介绍方法,看看输出结果是否正确。

3、使用面向对象的思想描述游戏中的怪物问题描述某公司要开发新游戏,请用面向对象的思 想设计怪物类,设定属性包括:怪物名字、生命值、攻击力、 防御力;方法包括:攻击的方法,防御的 方法.

​ 要求:

  • 通过构造函数实现属性赋值
  • 攻击方法,描述攻击状态。内容包括怪物名字,生命值,攻 击力
  • 防御方法通过输入参数接收攻击力。需要判断,如果攻击力小 于防御力,伤害值=0;反之伤害值=攻击力 -防御力 根据伤害值情 况,显示不同的防御信息。内容包括怪物名字, 防御力,防御后的生命值.
  • 编写测试方法,通过构造函数实例化怪物类的对象,并调用相关方法(测 试数据信息自定)运行效果:

image-20220117152232586

4、简述你对this关键字的理解

5、说说构造方法的特点

6、以下方法执行的结果是什么?

image-20220122195852734

image-20220122195900467

七、继承

1、继承的含义

image-20220117152330642

设计如下类:狗狗类 企鹅类 猫类。可以使用继承来简化代码。

为什么需要继承:

​ 多个类中存在相同的属性和行为的时候,讲这些内容抽取到一个单独的一个类 中。那么多个类无需再定义这些属性和行为,只要继 承即可。这个单独的类称作父类,其他类叫做子 类。可满足 is - a 的关系即可用继承实现。使用extends实现继承。

作用:

  • 使用继承的出现减少了代码冗余,提高的了代码的复用性。

  • 继承的出现,更有利于功能的扩展

  • 继承的出现让类和类之间产生了关系,提供了多态的前提。

子类继承了父类,就继承了父类的方法和属性。

在子类中,可以使用父类中定义的方法和属性,也可以创建新的数据和方法。

在java中,使用extends实现继承。子类继承父类,是对父类的扩展。

继承规则:

  • private修饰的不能继承。构造方法不继承。
  • java是单继承和多层继承。不能多继承。
  • 一个子类只能继承一个父类
  • 一个父类可以有多个子类。

练习:

1、设计学生类,学生类继承自人类,人类中包含属性,姓名,性别,年龄,有输出人基本 信息的方法。学生继承了人类,还有属性,学号,成绩。同时也输出学生的信息

2、super

super作用

  • 在Java类中使用super来调用父类中的指定操作:
  • super可用于访问父类中定义的属性
  • super可用于调用父类中定义的成员方法
  • super可以用在子类构造器中调用父类中的构造器
    • 尤其当父子类出现同名成员时,可以用super表明调用的是父类中的成员
  • super的追溯不仅限于直接父类。
  • super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识

image-20220122195938436

通过super调用父类的构造器:

  • 子类中所有的构造器默认都会访问父类中空参数的构造器

  • 当父类中没有空参数的构造器时,子类的构造器必须通过this(参数列表)或者super(参数列表) 语句指定调用本类或者父类中相的构造器。同时,只能”二选一”,且必须放在构造器的首行

  • 如果子类构造方法即无显示调用父类活本类的其他构造方法,且父类中又没有无参构造方法, 则编译错误。

    image-20220122195957571

    image-20220122200004912

image-20220122200017608

父类的构造方法都会被执行到。

this与super的区别

image-20220117152858990

3、方法重写

在子类中可以根据需要对从父类中继承来的方法进行改造,也称为方法的重置、覆盖。在程序执 行时,子类的方法将覆盖父类的方法。

  1. 子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表

  2. 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型

  3. 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限子类不能重写父类 中声明为private权限的方法

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

  5. static方法是属于类的,子类无法覆盖父类的方法。

    image-20220122200044178

    image-20220122200051484

    image-20220122200100231

练习:

1、某汽车租赁公司出租多种车辆,车型及租金情况如下:

image-20220122200247448

image-20220122200259027

4、访问修饰符

image-20220117153045707

5、对象实例化过程

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

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

3>当我们通过子类的构造方法创建子类对象时,我们一定会直接或间接的调用其父类的构造方法, 进而调用父类的构造方法,直到调用了Object类中的无参构造方法为止。真是因为加载过所有的父类结 构,所以才可以看到内存中有父类中的结构,子类对象才可以考虑进行调用。虽然子类调用了父类的构 造方法,但是并没有创建父类的对象。

无论通过哪个构造器创建子类对象,需要保证先初始化父类。

6、抽象类

抽象类用abstract修饰。抽象类不能直接new对象。抽象类中的方法不一定都是抽象方法.

image-20220122200325590

抽象类的特点

  1. 抽象类和抽象方法都需要被abstract修饰。抽象方法一定要定义在抽象类中。
  2. 抽象类不可以创建实例,原因:调用抽象方法没有意义。
  3. 只有覆盖了抽象类中所有的抽象方法后,其子类才可以实例化。否则该子类还是一个抽象类。

之所以继承,更多的是在思想,是面对共性类型操作会更简单

细节问题:

  1. 抽象类一定是个父类?是的,因为不断抽取而来的。

  2. 抽象类是否有构造函数?有,虽然不能给自己的对象初始化,但是可以给自己的子类对象初始化。 抽象类和一般类的异同点:

    相同:它们都是用来描述事物的。它们之中都可以定义属性和行为。

    不同:一般类可以具体的描述事物。抽象类描述事物的信息不具体抽象类中可以多定义一个成员:抽象 函数。一般类可以创建对象,而抽象类不能创建对象。

  3. 抽象类中是否可以不定义抽象方法。是可以的,那这个抽象类的存在到底有什么意义呢?仅仅是不 让该类创建对象。

  4. 抽象关键字abstract不可以和哪些关键字共存?final:fianl修饰的类是无法被继承的,而abstract 修饰的类一定要有子类.final修饰的方法无法被覆盖,但是abstract修饰的方法必须要被子类去实现 的。static:静态修饰的方法属于类的,它存在与静态区中,和对象就没关系了。而抽象方法没有 方法体,使用类名调用它没有任何意义。private:私有的方法子类是无法继承到的,也不存在覆 盖,而abstract和private一起使用修饰方法,abstract既要子类去实现这个方法,而private修饰子 类根本无法得到父类这个方法。互相矛盾。

  5. 抽象类是不能使用new操作符来初始化的。但是,仍然可以定义它的构造方法,这构造函数在它的 子类的构造函数中调用。即使子类的父类是具体的,这个子类也可以是抽象的。不能使用new操作 符从一个抽象类创建一个实例,但是抽象类可以用作一种数据类型

image-20220122200354260

7、抽象方法

用abstract修饰的方法是抽象方法。

抽象方法没有方法体。

抽象方法没有方法体 抽象方法必须在抽象类里

抽象方法必须在子类中被实现,除非子类是抽象类

抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行 扩展、改造,但子类总体上会保留抽象类的行为方式。

解决的问题: 当功能内部一部分实现是确定的,一部分实现是不确定的。这时可以把不确定的部分暴露 出去,让子类去实现。

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

8、final

final可以修饰属性,方法,类

final修饰的属性是常量

final修饰的方法,不能被重写,可以被继承

final修饰的类不能被继承

image-20220122200436562

练习:

1、修改Pet类为抽象类,Pet类中的eat方法为抽象方法,输出Dog信息

2、设计Bird、Fish类,都继承自抽象类Animal,实现其方法info(),并打印它们的信息

image-20220122200547931

3、Engine(引擎),具有power(功率,整数)属性,相对应的setter和getter方法,work()方法:输出”xx功率 的发动机在运转”。Car(Engine轿车),具有Engine属性,相对应的setter和getter方法,run()方法,在方法中 判断Engine对象是否为null,选择输出”发动机发动不了”或者”xx功率的发动机在运转,汽车在跑”。Benz(奔 驰),继承Car类,重写run()方法Driver(驾驶员),具有属性name(姓名),相对应的setter和getter方 法,driveCar(Benz benz)方法,在方法中输出“xxx 在开车”,并调用benz的run()方法。

4、写一个名为 Account 的类模拟账户。该类的属性和方法如下图所示。该类包括的属性:账号 id,余额 balance,年利率 annualInterestRate;包含的方法:访问器方法(getter 和setter 方法), 返回月利率的方法 getMonthlyInterest(),取款方法 withdraw(),存款方法deposit()。创建 Account 类 的一个子类 CheckAccount 代表可透支的账户,该账户中定义一个属性overdraft 代表可透支限额。在 CheckAccount 类中重写 withdraw 方法,其算法如下:如果(取款金额<账户余额),可直接取款如果 (取款金额>账户余额),计算需要透支的额度判断可透支额 overdraft 是否足够支付本次透支需要,如 果可以将账户余额修改为 0,冲减可透支金额如果不可以提示用户超过可透支额的限额

5、java类是否可以多继承,怎么实现多继承?

6、代码阅读

image-20220122200617859

7、代码阅读题

image-20220122200628540

8、编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数wheels和车重weight。 小车类Car是Vehicle的子类,其中包含的属性有载人数loader。卡车类Truck是Car类的子类,其中包含 的属性有载重量payload。

9、定义员工类Employee,包含姓名、工号和工资,包含计算奖金方法bonus,普通员工和经理都 是员工,计算奖金的方法为工资*奖金系数,普通员工的奖金系数为1.5(常量),经理为2(常量),分 别实现bonus方法,创建对象测试。

10、封装一个西游记人物类Person:包含私有属性:姓名(name)、身高(height)、武器 (weapon)、种族(race),并提供get方法和set方法。在测试类中实例化三个对象:tangseng(唐 僧)、sunwukong(孙悟空)、baigujing(白骨精),分别设置他们的种族为:人族、仙族、妖族在 打印对象时,按照例如:“姓名:孙悟空;种族:仙族”的格式在控制台输出信息。提供一个技能方法 void skill(),这个方法根据当前对象name属性判断技能,如果是孙悟空,就打印“七十二变”,如果是唐 僧,就打印“紧箍咒”,如果是白骨精,就打印“九阴白骨爪”,在测试类中调用这个方法。

11、阅读如下代码

image-20220122200655676

image-20220122200707738

image-20220122200714985

八、多态

1.多态

生活中的多态:不同类型的打印机打印效果不同。

image-20220117153605257

同一种事物,由于条件不同,产生的结果也不同

生活中的多态:同一个引用类型,使用不同的实例而执行不同操作

父类的类型指向子类的实例

实现多态的方式称为–动态绑定

指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

多态的两种表现形式—重载与重写

多态的必要条件: 要有继承 要有重写 父类的引用指向子类对象

父类更通用,子类更具体

Pet pet = new Dog();

Pet pet = new Cat(); 这种方式称之为 向上转型

子类转换为父类:在进行向上转型后,会发现 子类中特有的属性与方法已经丢失。此时需要进行向下转型(强制类型转化)。(容易抛出ClassCastException异常)。为了防止以上异常的出现,可以使用 instanceof判断该实例究竟是哪个类的类型。

小结:

  • 多态可以减少类中代码量,可以提高代码的可扩展性和可维护性
  • 向上转型——子类转换为父类,自动进行类型转换
  • 向下转型——父类转换为子类,结合instanceof运算符进行强制类型转换

实现多态的两种方式

  • 使用父类作为方法形参实现多态

  • 使用父类作为方法返回值实现多态

  • 类型转换

基本数据类型:

  • 字节数小的数据类型可以自动转换成大的数据类型

    如long g=20; double d=12.0f

  • 大的数据类型需要强制转换成小的数据类型

    如 floate f=(float)12.0 int a=(int)1200L

复合数据类型:

  • 从子类到父类的类型转换可以自动进行

  • 从父类到子类的类型转换必须通过强制类型转换实现

  • 无继承关系的引用类型间的转换是非法的

  • 在转换前如不确定,可以使用instanceof操作符测试一个对象的类型 Object类

2、Object类

Object类是所有的Java类的基类,如果一个类没有继承父类,此时默认继承Object类。

image-20220122200906874

1、== 与equals方法

==:是一个运算符。可以用在基本数据类型和引用数据类型的变量中。如果比较的是基本数据类型, 比较两个变量保存的值是否相等。如果比较的是引用数据类型,比较两个对象的地址值是否相等。也就 是两个引用是否指向同一个对象。

equals:是Object类中的方法。只能用在引用数据类型。在Object类中与==是一样的,也是比较两个 对象的地址是否相等。但是String,Date,包装类 重写了equals方法,除去比较地址还会比较内容。

我们自己的类也可以重写equals方法,会比较类中的属性是否相等。

练习:

定义学生类,有属性,姓名,年龄,电话。如果属性都相等,我们则认为是同一个对象

2、toString()方法

toString()方法,打印对象的时候,默认执行toString方法。

重载和重写的区别:

重载,发生在同一个类中的,方法名相同,参数列表不同。与访问修饰符和返回值类型无关。

重写, 发生在继承关系中的,子类重写父类的方法。方法名相同,参数列表相同,访问修饰符不能比父 类的严格。返回值类型相同或是父类方法返回值类型的子类。

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

重写,要等到方法调用那一刻,解释运行器才会确定所要调用的具体方法,称为动态绑定。

练习:

1、什么是多态,谈谈你堆多态的理解,多态情况下方法执行

2、一个类可以有几个父类,一个父类可以由几个子类,子类能获取父类的构造方法吗?

3、重写重载的区别

4、super关键字 与this关键字

5、抽象类和抽象方法

6、final关键字

7、多态是编译时行为还是运行时行为?【运行时行为】

image-20220122201142450

8、编写Order类,有int型的orderId,String型的orderName,相应的getter()和setter()方法, 两个参数的构造器,重写父类的equals()方法:public boolean equals(Object obj),并判断测试类中创 建的两个对象是否相等。

9、父类GeometricObject代表几何形状,子类Circle代表圆形,MyRectangle代表矩形。定义一个 测试类GeometricTest,编写equalsArea方法测试两个对象的面积是否相等,编写 displayGeometricObject方法显示对象的面积。

10、编写立体图形类,SolidFigure,必须有属性高,定义圆柱体Cylinde,长方体Rectangular。实 现计算面机getArea和体积的方法getVolume。

3、static关键字

有些属性和方法,希望无论有多少个对象,共享内存中的一份数据。此时可以用static修饰我们的数 和方法。

static:静态的。可以修饰属性,方法,代码块以及内部类。

static代表静态的。如果使用static声明属性,则该属性称之为称之为静态属性。使用static声明的属 性对所有的对象共享。static修饰的属性属于类级别的。对于静态的属性更推荐 使用类名. 的方式直接调 用

static:可以修饰成员变量,叫做静态变量。

没用static修饰的变量教实例变量,或非静态变量。

实例变量:各个对象有自己的独立的非静态属性。一个对象实例属性值的更改,不会影响到其他 对象。

静态变量:属于类级别的,多个类的对象共享一个静态变量。当通过一个对象更改静态属性的时 候,其他对象获取的是修改后的结果。静态变量随着类的加载而加载,可以通过类名直接调用。静态变 量的加载早于对象的创建。类只会加载一次,静态变量也是加载一次,存在方法区的静态域中,方法区 中主要保存类的加载信息,静态域,常量池。

画图理解:

static:可以修饰方法,叫做静态方法。也是随着类的加载来加载,也是可以通过类名.方式来调 用。静态方法只能调用静态成员。在静态方法内,不能使用this,super关键字。

静态结构域类的结构的生命周期是相同的。

属性被多个对象共享可以使用static修饰。

单例模式:讲解

4、代码块

代码块分为三类,有 普通代码块 构造块 静态代码块

1) 普通代码块:普通代码块就是指直接在方法或语句中定义的代码块。(了解)

在方法中定义一个普通代码块。代码块中的num作用范围在大括号结束的时候,就完毕了。

2) 构造块:是直接写在类中的代码块

可以发现构造块的执行时间在构造方法之前。而且会发现在继续创建对象的时候,构造块还会执行。

  1. 可以有输出语句。
  2. 可以对类的属性、类的声明进行初始化操作。
  3. 除了调用非静态的结构外,还可以调用静态的变量或方法。
  4. 若有多个非静态的代码块,那么按照从上到下的顺序依次执行。
  5. 每次创建对象的时候,都会执行一次。且先于构造器执行。

3) 静态代码块:使用static关键字声明的代码块。在以上代码中加入静态代码块。 会发现静态代码块在构造块之前执行。 会发现,静态代码块只会加载一次。

  1. 可以有输出语句。

  2. 可以对类的属性、类的声明进行初始化操作。

  3. 不可以对非静态的属性初始化。即:不可以调用非静态的属性和方法。

  4. 若有多个静态的代码块,那么按照从上到下的顺序依次执行。

  5. 静态代码块的执行要先于非静态代码块

  6. 静态代码块随着类的加载而加载,且只执行一次。

提示:分别加入两个静态属性与两个非静态属性,给其中一个赋值。断点调试看整个代码的执行过程。 继承关系下的构造方法执行顺序

image-20220131095628326

继承关系下:父类静态资源->子类静态资源->父类非静态资源->父类构造方法->子类非静态资源->子类构造方法

九、接口

请大家使用面向对象的思想实现防盗门的功能,大家会怎么设计呢?

可以借助接口实现。接口是用interface 修饰的。

接口的特点:

  • 接口不可以被实例化
  • 实现类必须实现接口的所有方法
  • 实现类可以实现多个接口
  • 接口中的变量都是静态常量

使用程序描述USB接口

image-20220117154100596

接口表示一种能力,接口的能力体现在接口的方法上。

面向接口编程

  • 在程序设计的时候,关心实现类有何能力,而不关心实现细节

  • 如果继承用 is-a 的形式来形容,那么接口就是 has-a

  • 一个人可以具有多项能力

接口的特点:

  • 一个类可以实现多个接口

接口有比抽象类更好的特性:

  1. 可以被多继承

  2. 设计和实现完全分离

  3. 更自然的使用多态

  4. 更容易搭建程序框架

  5. 更容易更换实现

Java中的接口

属性全都是全局静态常量

方法都是全局抽象方法

无构造方法

一个类可以实现多个接口,非抽象类实现接口时必须实现接口中的全部方法

抽象类利于代码复用,接口利于代码维护.

接口可以继承接口,而且可以继承多个接口。

image-20220117154420893

JDK8中接口的新特点

Java 8中,你可以为接口添加静态方法和默认方法。从技术角度来说,这是完全合法的,只是它看起来 违反了接口作为一个抽象定义的理念。

静态方法:使用 static 关键字修饰。可以通过接口直接调用静态方法,并执行其方法体。我们经常在相 互一起使用的类中使用静态方法。

默认方法:默认方法使用 default 关键字修饰。可以通过实现类对象来调用。我们在已有的接口中提供 新方法的同时,还保持了与旧版本代码的兼容性。

若一个接口中定义了一个默认方法,而另外一个接口中也定义了一个同名同参数的方法(不管此方法是 否是默认方法),在实现类同时实现了这两个接口时,会出现:接口冲突。解决办法:实现类必须覆盖 接口中同名同参数的方法,来解决冲突。

若一个接口中定义了一个默认方法,而父类中也定义了一个同名同参数的非抽象方法,则不会出现冲突 问题。因为此时遵守:类优先原则。接口中具有相同名称和参数的默认方法会被忽略。

练习:

1、牛犇有两个很好的朋友,一个是中国的王小强,喜欢吃四川菜,打太极一个是美国的约翰,喜 欢吃披萨,打橄榄球。每当朋友来拜访的时候,牛犇就会按照他们的喜欢招待他们。

2、1)下面关于接口的说法中不正确的是()。

​ A.接口中所有的方法都是抽象的

​ B.接口中所有的方法都是public访问权限

​ C.子接口继承父接口所用的关键字是implements

​ D.接口是Java中的特殊类,包含常量和抽象方法

​ 2)Java语言接口间的继承关系是()。

​ A.单继承 B.多重继承 C.不能继承 D.不一定

​ 3)一个类实现接口的情况是()。

​ A.一次可以实现多个接口 B.一次只能实现一个接口

​ C.不能实现接口 D.不一定

3、接口【abstract class】和抽象类【interface】的区别

4、接口是否可继承接口? 抽象类是否可实现(implements)接口? 抽象类是否可继承实体类(concrete class)?

5、代码阅读,以下代碼是否正確?

image-20220122201950144

十、内部类

在java中,允许一个类的定义位于另一个类的内部,前者称为内部类。

内部类与外部封装他的类之间存在逻辑上的所属关系。

Inner class 一般用在定义他的类或语句块之内,在外边引用他时必须给出完整的名字。Inner class 的名字不能与包含他的类的名相同。

当一个事物的内部,还有一个部分需要一个完整的结构进行描述,而这个内部的完整的结构又只为外 部事物提供服务,那么整个内部的完整结构最好使用内部类。

在Java中,允许一个类的定义位于另一个类的内部,前者称为内部类,后者称为外部类。

Inner class一般用在定义它的类或语句块之内,在外部引用它时必须给出完整的名称。

Inner class的名字不能与包含它的外部类类名相同;

分类: 成员内部类(static成员内部类和非static成员内部类) 局部内部类不谈修饰符)、匿名内部 类

1、成员内部类

成员内部类作为类的成员的角色:

和外部类不同,Inner class还可以声明为privateprotected

可以调用外部类的结构

Inner class 可以声明为static的,但此时就不能再使用外层类的非static的成员变量;

成员内部类作为类的角色:

可以在内部定义属性、方法、构造器等结构

可以声明为abstract类 ,因此可以被其它的内部类继承

可以声明为final

编译以后生成OuterClass$InnerClass.class字节码文件(也适用于局部内部类)

【注意】

  1. 非static的成员内部类中的成员不能声明为static的,只有在外部类或static的成员内部类中才可声 明static成员。

  2. 外部类访问成员内部类的成员,需要“内部类.成员”或“内部类对象.成员”的方式

  3. 成员内部类可以直接使用外部类的所有成员,包括私有的数据

  4. 当想要在外部类的静态成员部分使用内部类时,可以考虑内部类声明为静态的

    image-20220122202303024

    image-20220122202309846

    Inner class可以声明为抽象类 ,因此可以被其它的内部类继承。也可以声明为final的。和外层类不 同,Inner class可以声明为private或protected;Inner class 可以声明为static的,但此时就不能 再使用外层封装类的非static的成员变量;(静态类的使用直接 :A.C c = new A.C();)非static的 内部类中的成员不能声明为static的,只有在顶层类或static的内部类中才可声明static成员;

    为什么要使用内部类?在《Think in java》中有这样一句话:使用内部类最吸引人的原因是:每个 内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实 现,对于内部类都没有影响。 在我们程序设计中有时候会存在一些使用接口很难解决的问题,这个 时候我们可以利用内部类提供的、可以继承多个具体的或者抽象的类的能力来解决这些程序设计问 题。可以这样说,接口只是解决了部分问题,而内部类使得多重继承的解决方案变得更加完整。使 用内部类最大的优点就在于它能够非常好的解决多重继承的问题,但是如果我们不需要解决多重继 承问题,那么我们自然可以使用其他的编码方式,但是使用内部类还能够为我们带来如下特性(摘 自《Think in java》):

    1、内部类可以用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独 立。

    2、在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。

    3、创建内部类对象的时刻并不依赖于外围类对象的创建。

    4、内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。

    5、内部类提供了更好的封装,除了该外围类,其他类都不能访问。

    成员内部类也是最普通的内部类,它是外围类的一个成员,所以他是可以无限制的访问外围类的所 有 成员属性和方法,尽管是private的,但是外围类要访问内部类的成员属性和方法则需要通过内 部类实例来访问.

    image-20220122202418732

    image-20220122202424887

    在成员内部类中要注意两点,第一成员内部类中不能存在任何static的变量和方法;第二 成员内部类是依附于外围类的,所以只有先创建了外围类才能够创建内部类。

    成员内部类之静态内部类

    static修饰的内部类我们称之为静态内部类,不过我们更喜欢称之为嵌套内部类。静态内部类与非 静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之后会隐含地保存着一个 引用,该引用是指向创建它的外围内,但是静态内部类却没有。没有这个引用就意味着:它的创建 是不需要依赖于外围类的。它不能使用任何外围类的非static成员变量和方法。

    image-20220122202503538

    image-20220211205832822

    image-20220211205845431

    image-20220211205901809

    image-20220211205911497

2、局部内部类

局部嵌套类,简称局部类,局部类所属范围:在块、构造器以及方法内,这里的块包括普通块和静 态块。局部类只在本块范围内有效。局部类是嵌套类,但不是成员类,而且有名称(不是匿名 类)。 只能在声明它的方法或代码块中使用,而且是先声明后使用。除此之外的任何地方都不能使用该类 但是它的对象可以通过外部方法的返回值返回使用,返回值类型只能是局部内部类的父类或父接口 类型

image-20220122202618825

内部类仍然是一个独立的类,在编译之后内部类会被编译成独立的.class文件,但是前面冠以外部 类的类名和$符号,以及数字编号。、 只能在声明它的方法或代码块中使用,而且是先声明后使用。除此之外的任何地方都不能使用该 类。

局部内部类可以使用外部类的成员,包括私有的。

局部内部类可以使用外部方法的局部变量,但是必须是final的。由局部内部类和局部变量的声明周 期不同所致。

局部内部类和局部变量地位类似,不能使用public,protected,缺省,private

局部内部类不能使用static修饰,因此也不能包含静态成员

3、匿名内部类

匿名类,就是没有名称的类,其名称由Java编译器给出,一般是形如:外部类名称+$+匿名类顺 序,没有名称也就是其他地方就不能引用,不能实例化,只用一次,当然也就不能有构造器。 匿名类根据位于地方不同分为:成员匿名类和局部匿名类。

匿名类不能使用任何关键字和访问控制符,匿名类和局部类访问规则一样,只不过内部类显式的定 义了一个类,然后通过new的方式创建这个局部类实例,而匿名类直接new一个类实例,没有定义 这个类。匿名类最常见的方式就是回调模式的使用,通过默认实现一个接口创建一个匿名类然后, 然后new这个匿名类的实例。

image-20220122202803546

十一、异常

1、异常概述与体现结构

​ 异常:在Java语言中,将程序执行中发生的不正常情况称为“异常”。 (开发过程中的语法错误和逻 辑错误不是异常)

Java程序在执行过程中所发生的异常事件可分为两类:

​ 1>Error:Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。比 如:StackOverflowError和OOM。一般不编写针对性的代码进行处理。

​ 2>Exception:其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行 处理。例如:空指针异常,数组下标越界异常。

image-20220211210016410

​ 3>Exception下异常又可以分为 运行时异常 和 编译时异常

​ 1)运行时异常是指编译器不要求强制处置的异常。一般是指编程时的逻辑错误,是程序员应该 积极避免其出现的异常。java.lang.RuntimeException类及它的子类都是运行时异常。对于这类异 常,可以不作处理,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。

​ 2)是指编译器要求必须处置的异常。即程序在运行时由于外界因素造成的一般性异常。编译 器要求Java程序必须捕获或声明所有编译时异常。对于这类异常,如果程序不处理,可能会带来意想不 到的结果。

2、异常处理机制

image-20220211210125322

​ Java采用的异常处理机制,是将异常处理的程序代码集中在一起,与正常的程序代码分开,使得 程序简洁、优雅,并易于维护

​ 1>捕获异常 try-catch-finally:如果一个方法内抛出异常,该异常对象会被抛给调用者方法中处 理。如果异常没有在调用者方法中处理,它继续被抛给这个调用方法的上层方法。这个过程将一直继续 下去,直到异常被处理。这一过程称为捕获(catch)异常。 如果一个异常回到main()方法,并且main()也 不处理,则程序运行终止。 程序员通常只能处理Exception,而对Error无能为力。

image-20220211210200278

try捕获异常的第一步是用try{…}语句块选定捕获异常的范围,将可能出现异常的代码放在try语句块 中。

catch (Exceptiontype e)在catch语句块中是对异常对象进行处理的代码。每个try语句块可以伴随 一个或多个catch语句,用于处理可能产生的不同类型的异常对象。

捕获异常的有关信息:与其它对象一样,可以访问一个异常对象的成员变量或调用它的方法。

getMessage() 获取异常信息,返回字符串

printStackTrace() 获取异常类名和异常信息,以及异常出现在程序中的位置。返回值void。

finally 捕获异常的最后一步是通过finally语句为异常处理提供一个统一的出口,使得在控制流转到 程序的其它部分以前,能够对程序的状态作统一的管理。

不论在try代码块中是否发生了异常事件,catch语句是否执行,catch语句是否有异常,catch语句 中是否有return,finally块中的语句都会被执行。finally语句和catch语句是任选的。

前面使用的异常都是RuntimeException类或是它的子类,这些类的异常的特点是:即使没有使用try 和catch捕获,Java自己也能捕获,并且编译通过( 但运行时会发生异常使得程序运行终止 )。

如果抛出的异常是IOException等类型的非运行时异常,则必须捕获,否则编译错误。也就是说,我 们必须处理编译时异常,将异常进行捕捉,转化为运行时异常

  1. 正常情况

image-20220211210307721

  1. 异常情况

    image-20220211210335833

  2. 异常类型不匹配

image-20220211210343959

  1. finally:在try/catch块后加入finally块,可以确保无论是否发生异常,finally块中的代码总能 被执行.

    image-20220211210402237

2>声明异常:声明抛出异常是Java中处理异常的第二种方式。

如果一个方法(中的语句执行时)可能生成某种异常,但是并不能确定如何处理这种异常,则此方法 应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。在方法 声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型, 也可以是它的父类。

3>抛出异常Java程序的执行过程中如出现异常,会生成一个异常类对象,该异常对象将被提交给 Java运行时系统,这个过程称为抛出(throw)异常。

3、自定义异常一般地,用户自定义异常类都是RuntimeException的子类。 自定义异常类通常需要 编写几个重载的构造器。 自定义异常需要提供serialVersionUID 自定义的异常通过throw抛出。 自定义 异常最重要的是异常类的名字,当异常出现时,可以根据名字判断异常类型。

练习:

1、运行时异常与一般异常有何异同

2、Java中的异常处理机制的简单原理和应用

3、JAVA语言如何进行异常处理,关键字:throws,throw,try,catch,finally分别代表什么意义?在 try块中可以抛出异常吗?

4、try {}里有一个return语句,那么紧跟在这个try后的finally {}里的code会不会被执行,什么时 候被执行,在return前还是后?

5、error和exception有什么区别。

6、写出以下代码的运行结果

image-20220220200840313

7、写出以下代码的结果

image-20220220200925619

8、写出程序结果

image-20220220200950637

9、写出如下代码的运行结果

image-20220220201006466

10、写出如下代码的运行结果

image-20220220201020201

image-20220220201030946

十二、常用类

1、基本数据类和引用数据类型

八种基本数据类型有对应的包装类型,认识基本数据类型与包装类型之间的转化。同时知道数组转化为 字符串,以及将字符串转化为数值类型。

装箱:将基本数据类型转化为包装类型称之为装箱

int i = 500; Integer t = new Integer(i);

Float f = new Float(“4.56”);

拆箱:将包装类型转化为基本数据类型称之为拆箱

boolean b = bObj.booleanValue();

字符串转换成基本数据类型

通过包装类的构造器实现:int i = new Integer(“12”);

通过包装类的parseXxx(String s)静态方法:Float f = Float.parseFloat(“12.1”);

基本数据类型转换成字符串 调用字符串重载的valueOf()方法:String fstr = String.valueOf(2.34f);

更直接的方式:String intStr = 5 +

image-20220220201207616

练习:

1、写出如下代码的结果

image-20220220201218906

2、String类

String类代表字符串。Java程序中的所有字符串字面值都是此类的实例实现。String类是一个final 类,代表不可变的字符串。字符串是常量,用双引号引起来表示。他们的值在创建后不能更改。Sting对 象的字符串内容是存储在一个字符数组value[]中的。String如果直接赋值,是将字符串保存到方法区中 的常量池中。如果new,会在堆中,然后堆再指向方法区常量池中的字符串。字符串存储再常量池中的目 的是共享。只要其中有一个是变量,结果就在堆中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
结论:
常量与常量的拼接结果在常量池。且常量池中不会存在相同内容的常量.
只要其中有一个是变量,结果就在堆中
如果拼接的结果调用intern()方法,返回值就在常量池中
String s1 = "a";
说明:在字符串常量池中创建了一个字面量为"a"的字符串。
s1 = s1 + "b";
说明:实际上原来的“a”字符串对象已经丢弃了,现在在堆空间中产生了一个字符串s1+"b"(也就是"ab")。如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的性能。
String s2 = "ab";
说明:直接在字符串常量池中创建一个字面量为"ab"的字符串。
String s3 = "a" + "b";
说明:s3指向字符串常量池中已经创建的"ab"的字符串。
String s4 = s1.intern();
说明:堆空间的s1对象在调用intern()之后,会将常量池中已经存在的"ab"字符串赋值给s4。

字符串常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int length():返回字符串的长度: return value.length
char charAt(int index): 返回某索引处的字符return value[index]boolean isEmpty():判断是否是空字符串:return value.length == 0 String toLowerCase():使用默认语言环境,将 String 中的所有字符转换为小写
String toUpperCase():使用默认语言环境,将 String 中的所有字符转换为大写
String trim():返回字符串的副本,忽略前导空白和尾部空白
boolean equals(Object obj):比较字符串的内容是否相同
boolean equalsIgnoreCase(String anotherString):与equals方法类似,忽略大小写
String concat(String str):将指定字符串连接到此字符串的结尾。 等价于用“+”int compareTo(String anotherString):比较两个字符串的大小
String substring(int beginIndex):返回一个新的字符串,它是此字符串的从beginIndex开始截取到最后的一个子字符串。String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。
boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束
boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始
boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
boolean contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列时,返回 true
int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引
int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引
int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索
注:indexOf和lastIndexOf方法如果未找到都是返回-1
String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用newChar 替换此字符串中出现的所有 oldChar 得到的。
String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
String replaceAll(String regex, String replacement) : 使用给定的replacement替换此字符串所有匹配给定的正则表达式的子字符串。
String replaceFirst(String regex, String replacement) : 使用给定的replacement替换此字符串匹配给定的正则表达式的第一个子字符串。
boolean matches(String regex):告知此字符串是否匹配给定的正则表达式。
String[] split(String regex):根据给定正则表达式的匹配拆分此字符串。
String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个

String特点:

1
2
3
4
5
6
7
8
9
10
11
String:字符串,使用一对""引起来表示。
1.String声明为final的,不可被继承
2.String实现了Serializable接口:表示字符串是支持序列化的。
实现了Comparable接口:表示String可以比较大小
3.String内部定义了final char[] value用于存储字符串数据
4.String:代表不可变的字符序列。简称:不可变性。
体现:1.当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
2. 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
3. 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
5.通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
6.字符串常量池中是不会存储相同内容的字符串的。

3、StringBuffer类

java.lang.StringBuffer代表可变的字符序列,JDK1.0中声明,可以对字符串内容进行增删,此时不会产 生新的对象。很多方法与String相同。作为参数传递时,方法内部可以改变值。

StringBuffer类不同于String,其对象必须使用构造器生成。有三个构造器:

扩容问题:如果要添加的数据底层数组盛不下了,那就需要扩容底层的数组。

​ 默认情况下,扩容为原来容量的2倍 + 2,同时将原有数组中的元素复制到新的数组中。

1
2
指导意义:开发中建议大家使用:StringBuffer(int capacity) 或
StringBuilder(int capacity)
1
2
3
StringBuffer():初始容量为16的字符串缓冲区
StringBuffer(int size):构造指定容量的字符串缓冲区
StringBuffer(String str):将内容初始化为指定字符串内容

StringBuffer常用的方法

1
2
3
4
5
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()//把当前字符序列逆转

4、StringBuilder类

StringBuilder 和 StringBuffer 非常类似,均代表可变的字符序列,而且提供相关功能的方法也一样 面试题:对比String、StringBuffer、StringBuilder

String(JDK1.0):不可变字符序列

StringBuffer(JDK1.0):可变字符序列、效率低、线程安全

tringBuilder(JDK 5.0):可变字符序列、效率高、线程不安全

注意:作为参数传递的话,方法内部String不会改变其值,StringBuffer和StringBuilder会改变其值。

String、StringBuffer、StringBuilder三者的异同?

String:不可变的字符序列;底层使用char[]存储

StringBuffer:可变的字符序列;线程安全的,效率低;底层使用char[]存储

StringBuilder:可变的字符序列;jdk5.0新增的,线程不安全的,效率高;底层使用char[]存储。

练习:

1、画出内容结构

1
2
3
4
5
String s1="hello";
String s2="hello";
String s3=new String("hello");
s1+="world";

2、String是可变的吗?String可以被继承吗?为什么?

3、String s=new String(“hello”);再内存中创建了几个对象?

4、String,StringBuffer,StingBuilder三者对比

5、说出String类常用的方法

6、字符串反转,给定字符串,给定起始位置和结束位置,对字符串进行反转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static String reverse(String str,int start,int end){
char[]arr= str.toCharArray();
for(int i=start,j=end;i<j;i++,j--){
char c = arr[i];
arr[i]=arr[j];
arr[j]=c;
}
return new String(arr);
}
public static void main(String[] args) {
String s = reverse("helloworld",2,8);
System.out.println(s);
}

7、获取一个字符串再另一个字符串中出现的次数。比如“ab”在“abcccabddddabeeeeaaaaab“中出现的 次数”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static int getCount(String mainWords,String subWords){
if(mainWords.length()>subWords.length()){
int count=0;
int index;
int mainLength=mainWords.length();
int subLength=subWords.length();

while ((index=mainWords.indexOf(subWords))!=-1){
count++;
mainWords=mainWords.substring(index+subLength);
}
return count;
}else{
return 0;
}
}
public static void main(String[] args) {
int count = getCount("abcccabddddabeeeeaaaaab","ab");
System.out.println(count);
}

public static int getCount(String mainWords,String subWords){
if(mainWords.length()>subWords.length()){
int count=0;
int index=0;
while((index=mainWords.indexOf(subWords,index))!=-1){
count++;
index+=subWords.length();
}
return count;
}else{
return 0;
}
}

8、查找最大相同字串”aabbccadeahellocadfasfdas“,与字符串”aaellocc“最大相同字串是”elloc“。【只 有一个相同字符串】

1
2
3
4
5
6
7
8
9
10
11
12
public static String getSameWord(String maxStr,String minStr){
for(int i=0;i<minStr.length();i++){
for(int x=0,y=minStr.length()-i;y<=minStr.length();x++,y++){
String subStr = minStr.substring(x,y);
if(maxStr.contains(subStr)){
return subStr;
}
}
}
return null;
}

9、写出以下代码的运行结果

1
2
3
4
5
6
7
8
String str = null;
StringBuffer sb = new StringBuffer();
sb.append(str);
System.out.println(sb.length());
System.out.println(sb);
StringBuffer sb1 = new StringBuffer(str);
System.out.println(sb1);

5、日期类型

1>System类

System类提供的public static long currentTimeMillis()用来返回当前时间与1970年1月1日0时0分0秒之 间以毫秒为单位的时间差。此方法适于计算时间差。

2>java.util.Date类

Date():使用无参构造器创建的对象可以获取本地当前时间。

Date(long date):

常用的方法:

getTime():返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。

toString():把此 Date 对象转换为以下形式的 String: dow mon ddhh:mm:ss zzz yyyy 其中: dow 是 一周中的某一天 (Sun, Mon, Tue, Wed, Thu, Fri, Sat),zzz是时间标准

3>SimpleDateFormat类

Date类的API不易于国际化,大部分被废弃了,java.text.SimpleDateFormat类是一个不与语言环境有 关的方式来格式化和解析日期的具体类。

它允许进行格式化:日期文本、解析:文本日期

格式化: SimpleDateFormat() :默认的模式和语言环境创建对象

public SimpleDateFormat(String pattern):该构造方法可以用参数pattern指定的格式创建一个 对象,该对象调用:

**public String format(Date date)**:方法格式化时间对象date

解析: public Date parse(String source):从给定字符串的开始解析文本,以生成一个日期

image-20220220202002426

4> java.util.Calendar(日历)类

Calendar是一个抽象基类,主用用于完成日期字段之间相互操作的功能。

获取Calendar实例的方法

使用Calendar.getInstance()方法 调用它的子类GregorianCalendar的构造器。

一个Calendar的实例是系统时间的抽象表示,通过get(int field)方法来取得想要的时间信息。比如 YEAR、MONTH、DAY_OF_WEEK、HOUR_OF_DAY 、MINUTE、SECOND

public void set(int field,int value)

public void add(int field,int amount)

public final Date getTime()

public final void setTime(Date date)

注意

获取月份时:一月是0,二月是1,以此类推,12月是11

获取星期时:周日是1,周二是2 , 。。。。周六是7

6、Math类

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

7、BigInteger与BigDecimal

Integer类作为int的包装类,能存储的最大整型值为2 31-1,Long类也是有限的,最大为2 63-1。如果要 表示再大的整数,不管是基本数据类型还是他们的包装类都无能为力,更不用说进行运算了。

java.math包的BigInteger可以表示不可变的任意精度的整数。BigInteger 提供所有 Java 的基本 整数操作符的对应物,并提供 java.lang.Math 的所有相关方法。另外,BigInteger 还提供以下运算:模 算术、GCD 计算、质数测试、素数生成、位操作以及一些其他操作。

构造器 BigInteger(String val):根据字符串构建BigInteger对象

1
2
3
4
5
6
7
public BigInteger abs():返回此 BigInteger 的绝对值的 BigInteger。
BigInteger add(BigInteger val) :返回其值为 (this + val) 的 BigInteger
BigInteger subtract(BigInteger val) :返回其值为 (this - val) 的 BigInteger
BigInteger multiply(BigInteger val) :返回其值为 (this * val) 的 BigInteger
BigInteger divide(BigInteger val) :返回其值为 (this / val) 的 BigInteger。整数相除只保留整数部分。
BigInteger remainder(BigInteger val) :返回其值为 (this % val) 的 BigInteger。BigInteger[] divideAndRemainder(BigInteger val):返回包含 (this / val) 后跟(this % val) 的两个 BigInteger 的数组。
BigInteger pow(int exponent) :返回其值为 (thisexponent) 的BigInteger。

BigDecimal类

一般的Float类和Double类可以用来做科学计算或工程计算,但在商业计算中,要求数字精度比较高, 故用到java.math.BigDecimal类。BigDecimal类支持不可变的、任意精度的有符号十进制定点数。

8、Comparable接口与Compator接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public  class student implements Comparable<student>{
private Integer id;
private String name;
private String sex;
private Integer age;
private Integer score;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

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

public String getSex() {
return sex;
}

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

public Integer getAge() {
return age;
}

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

public Integer getScore() {
return score;
}

public void setScore(Integer score) {
this.score = score;
}

public student() {
}

public student(Integer id, String name, String sex, Integer age, Integer score) {
this.id = id;
this.name = name;
this.sex = sex;
this.age = age;
this.score = score;
}

@Override
public int compareTo(student s) {
return this.age.compareTo(s.age);
}
}

十三、线程

1、基本概念

​ 程序:为了完成特定任务,用某种语言编写的一组指令的集合。是一段静态的代码,静态对象。

​ 进程:是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程。有他自身的产生, 存在和消亡的过程。【生命周期】程序是静态的,进程是动态的。进程做为资源的分配单位,系统在运 行时会为每个进程分配不同的内存区域。

​ 线程:线程是程序的执行单元。是程序使用CPU的最基本单位。一个进程的执行,是靠线程来走 的。

​ 程序可以进一步细化为线程,是一个程序内部的一条执行路径。若一个进程同一时间并行执 行多个线程,就是支持多线程的。线程做为调度和执行的单位,每个线程拥有独立的运行栈和程序计算 器,线程切换的开销小。一个进程中的多个线程共享相同的内存单元|内存地址。他们从同一堆中分配对 象,可以访问相同的变量和对象。这就使得线程间通信更简洁,高效。但多个线程操作共享的系统资源 可能会带来安全隐患。

​ 单核CPU和多核CPU:单核CPU,其实是一种假的多线程。因为再一个时间单元内,也只能执行一 个线程的任务。因为CPU时间单元特别短,因此感觉不出来。多核的话,才能更好的发挥多线程的效 率。一个java程序java.exe,至少三个线程main()主线程,gc()垃圾回收线程,异常处理线程。如果发生异 常,会影响主线程。

​ 并行域并发:并行: 多个CPU同时执行多个任务。也就是 多人同时做不同的事情。

​ 并发:一个CPU(采用时间片)同时执行多个任务。比如,秒杀,多人做同一件事。

​ 多线程的优点:以单核CPU为例,只使用单个线程 先后完成多个任务(调用多个方法),肯定比 用多个线程来完成用的时间更短,为何仍徐多线程呢?多线程的优点:1>提供应用程序的响应,对图形 化界面更有意义,可增强用户体验。提供计算机系统CPU的利用率。改善程序结构,将既长又复杂的进 程分为多个线程,独立运行,利用理解和修改。

​ 什么时候需要多线程呢?程序需要同时执行两个或多个任务

​ 程序需要实现一些需要等待的任务时,如用户输入,文件读写操作,网络操作,搜索等。

​ 需要一些后台执行的程序时。

2、 线程的创建和使用

​ 之前的程序都是单线程的。

​ 方式一: JAVA语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MyThread01 extends Thread {
@Override
public void run() {
for(int i=1;i<=100;i++) {
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}

public class MyThread01Test {

public static void main(String[] args) {
MyThread01 thread01 = new MyThread01();
thread01.setName("线程1");
//thread01.run();//不会启动线程的,现在的程序还是单线程的程序
thread01.start();//启动线程

MyThread01 thread02 = new MyThread01();
thread02.setName("线程2");
thread02.start();//启动线程

thread01.start();//启动线程

for(int i=1;i<=100;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
     Thread类的特点:每个线程都说通过某个特定的Thread对象的run()方法来完成操作的,经常把run() 方法的主体称为线程体。我们再使用的时候,是通过调用start()方法来启动这个线程的。 一个线程对象 只能调用一次start()方法启动线程。如果重复调用就会抛出异常"IllegalThreadStateException" 

​ 方式二:实现Runnable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyThread02 implements Runnable {
private String name;
public MyThread02(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(name + ":" + i);
}
}
}
public static void main(String[] args) {
MyThread02 thread01 = new MyThread02("线程01");
MyThread02 thread02 = new MyThread02("线程02");
Thread t1 = new Thread(thread01);
Thread t2 = new Thread(thread02);
t1.start();
t2.start();
}

3、线程状态

​ 新建(New):在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此 时,他已经有了相应的内存空间和其他资源,但还不处于可运行状态。

​ 就绪(Runnable):新建线程对象后,可调用线程的start()方法就可以启动线程。当线程启动时, 线程进入就绪状态。此时,线程将进入线程队列排队,等待CPU服务,这表名他已经具备了运行条件

​ 运行(Running):当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此 时,自动调用该线程对象的run()方法。run()方法定义了该线程的操作和功能。

​ 阻塞(Blocked):一个正在执行的线程在某些特殊情况下,如被认为挂起或需要执行耗时的输入/ 输出操作时,会让出CPU并暂时终止自己的行为,进入堵塞状态,在可执行状态下,如果调用 sleep(),suspend().wait()等方法,线程将进入堵塞状态。堵塞时,线程不能进入排队队列,只有当引起 堵塞的原因被消除后,线程才可以转入就绪状态。线程调用了sleep()方法主动放弃所占用的处理器资 源。线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。线程试图获得一个同步监视器,但 该同步监视器正被其它线程所持有。线程在等待某个通知(notify)。

​ 死亡(Dead):线程调用stop()方法时或run()方法执行结束后,处于死亡状态。处于死亡状态的线 程不具备继续运行的能力。

image-20220220202058387

一些方法:

1>join方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyThread implements Runnable{
@Override
public void run() {
for(int i=1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+ ":"+i);
}
}
}
public static void main(String[] args) {
MyThread thread = new MyThread();
Thread th1 = new Thread(thread, "线程1");
Thread th2 = new Thread(thread, "线程2");
Thread th3 = new Thread(thread, "线程3");
th1.start();
try {
th1.join();//等待th1运行完毕后,其他线程才执行
} catch (InterruptedException e) {
e.printStackTrace();
}
th2.start();
th3.start();
for(int i=1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}

2> sleep方法

​ 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统 计时器和调度程序精度和准确性的影响。该线程不丢失任何监视器的所属权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyThread implements Runnable{
@Override
public void run() {
for(int i=1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+ ":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
MyThread thread = new MyThread();
Thread th1 = new Thread(thread, "线程1");
th1.start();
for(int i=1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}

3>Interrupt 线程被中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MyThread implements Runnable{
@Override
public void run() {
System.out.println("开始执行:"+new Date());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("线程被中断");
}
System.out.println("程序结束:"+new Date());
}
}

public class MyThreadTest {

public static void main(String[] args) {
MyThread thread = new MyThread();
Thread th1 = new Thread(thread, "线程1");
th1.start();
try {
Thread.sleep(2000);
th1.interrupt();//线程直接被中断了。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

4> yield

​ 暂停当前正在执行的线程对象,并执行其他线程。yield()应该做的是让当前运行线程 回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同 优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还 有可能被线程调度程序再次选中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
MyThread run = new MyThread();
Thread thread = new Thread(run, "线程1");
thread.start();
for(int i=0;i<100;i++){
String name = Thread.currentThread().getName();
System.out.println(name+":"+i);
Thread.yield();
}
}
}

5>join方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyThread implements Runnable{
@Override
public void run() {
for(int i=1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class MyThredTest {
public static void main(String[] args) {
MyThread run = new MyThread();
Thread th1 = new Thread(run, "线程1");
Thread th2 = new Thread(run, "线程2");
Thread th3 = new Thread(run, "线程3");
th1.start();
try {
th1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
th2.start();
th3.start();
}
}

6>线程的优先级

​ 线程默认优先级是5,可以自己设置线程的优先级。通过,setPriority()方 法来设置线程的优先级。优先级的范围是1-10.线程优先级高仅仅表示线程获取CPU时间片的几率高,但 是要在次数比较多,或者多此运行时才能看比较好的效果。

线程分为两类,一类是守护线程,一类是用户线程。他们在几乎每个方面都说相同的,唯一的 区别是判断JVM何时离开。守护线程是用来服务用户线程的。通过在start()方法前调用

thread.setDaemon(true)可以把一个用户线程变成守护线程。Java垃圾回收就是一个典型的守护线程。 弱JVM中都是守护线程,当前JVM将退出。

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
MyThread run = new MyThread();
Thread th1 = new Thread(run, "线程1");
Thread th2 = new Thread(run, "线程2");
Thread th3 = new Thread(run, "线程3");
th1.setPriority(10);
th3.setPriority(1);
th1.start();
th2.start();
th3.start();
}

4、同步域死锁

​ 创建三个窗口买票,总票数为60张票。按照之前的代码,会发现代码出现问题,又相同的票或者 负数出现。出现问题的原因是当某个线程操作车票的过程中,操作尚未完成,其他线程进入,也操作了 该车票。如何解决?当一个线程a在操作ticket的时候,其他线程不能参与进来,直到线程a操作完成, 其他线程才可以操作。可以采用同步解决该问题。

1
2
3
4
synchronized(同步监视器){
//需要被同步的代码
}

操作共享数据的代码,即为需要被同步的代码,不能多包含也不能少包含。

共享数据:多个线程共同操作的变量

同步监视器:俗称,锁。任何一个类的对象,都可以充当锁,但是要去必须多个线程公用一把锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Tickets implements Runnable {
private static int num = 60;

@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
synchronized (this) {
if (num <= 0) {
break;
}
System.out.println(Thread.currentThread().getName() + "再售" + num);
num--;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TicketsTest {

public static void main(String[] args) {

Tickets run = new Tickets();
Thread th1 = new Thread(run,"窗口1");
Thread th2 = new Thread(run,"窗口2");
Thread th3 = new Thread(run,"窗口3");
th1.start();
th2.start();
th3.start();
}
}

​ 同步的特点:前提:多个线程。

​ 解决问题的时候要注意:多个线程使用的是同一个锁对象。

​ 同步的好处:同步的出现解决了多线程的安全问题。

​ 同步弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会 降低程序的运行效率。

synchronized: 同步,需要被同步的代码。

​ 同步方法,仍然需要同步监视器,只是不需要显示声明

​ 非静态的同步方法,同步监视器是this

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Singleton{
static Singleton singleton;

private Singleton(){

}

public synchronized static Singleton getInstance(){
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
}

public class Singleton{
static Singleton singleton;

private Singleton(){

}

public static Singleton getInstance(){
if(singleton==null){
synchronized(Singletion.class){
if(singleton==null){
singleton = new Singleton();
}
}
}
return singleton;
}
}

​ 死锁 : 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源, 就形成了死锁。出现死锁后,不会出现异常,不会出现提升,只是所有的线程都处于阻塞状态,无法继 续。解决办法:专门的算法,原则。尽量减少同步资源的定义,尽量避免嵌套同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class DeadLock {
static String s1="hello";
static String s2 = "world";
public static void main(String[] args) {
Lock1 lock1 = new Lock1();
Thread t1 = new Thread(lock1);

Lock2 lock2 = new Lock2();
Thread t2 = new Thread(lock2);

t1.start();
t2.start();
}
}

public class Lock2 implements Runnable{
@Override
public void run() {
while(true){
try {
System.out.println("Lock2执行");
synchronized (DeadLock.s2) {
System.out.println("Lock2持有s2的锁");
Thread.sleep(3000);
synchronized (DeadLock.s1) {
System.out.println("Lock2持有s1锁");
}
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public class Lock1 implements Runnable{
@Override
public void run() {
while(true){
System.out.println("Lock1执行");
try {
synchronized (DeadLock.s1) {
System.out.println("Lock1持有s1的锁");
Thread.sleep(3000);
synchronized (DeadLock.s2) {
System.out.println("Lock1持有s2锁");
}
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

如何避免死锁呢?加锁顺序(线程按照一定的顺序加锁)。加锁时限(线程尝试获取锁的时候加上一定 的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)。死锁检测。

Lock(锁): 从JDK5.0开始,java提供了更强大的线程同步机制。通过显示定义同步锁对象来实现同步。同 步锁使用Lock对象充当。该接口是控制多个线程对共享资源进行访问的公交。锁提供了对共享资源的独 占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

ReentrantLock类实现了Lock接口,他拥有与synchronized相同的并发性盒内存语义,再实现线程安全 的控制中,比较常用的RenntrantLock可以显示的加锁和释放锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Window implements Runnable{

private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();

@Override
public void run() {
while(true){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
try{
lock.lock();
if(ticket > 0){
//2.调用锁定方法lock()
System.out.println(Thread.currentThread().getName() +":售票,票号为:" + ticket);
ticket--;
}else{
break;
}
}finally {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}
}

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

练习:

两个用户分布向一个账户存3000元,每次存1000,存3次。每次存款后打印账户余额

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class Account {
private int balance;

public int getBalance() {
return balance;
}

public void setBalance(int balance) {
this.balance = balance;
}

public synchronized void inMoney(int money){
this.balance = this.balance+money;

System.out.println(Thread.currentThread().getName()+"存款,卡中余额:"+balance);
}
}
class Customer extends Thread{
Account account;

public Customer(Account account) {
this.account = account;
}

@Override
public void run() {
for(int i=0;i<3;i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.inMoney(1000);
}
}
}
class CustomerTest{
public static void main(String[] args) {
Account account = new Account();
Customer cus1 = new Customer(account);
cus1.setName("A");
Customer cus2 = new Customer(account);
cus2.setName("B");
cus1.start();
cus2.start();
}
}

5、线程之间的通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.openlab.test;

public class PrintNum implements Runnable{
static int i=1;
@Override
public void run() {

while(true){

try {
Thread.sleep(100);

// wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this){
notify();
if(i>100){
break;
}
System.out.println(Thread.currentThread().getName()+":"+i);
i++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}
}
}
class PrintNumTest{
public static void main(String[] args) {
PrintNum run = new PrintNum();
Thread th1 = new Thread(run,"线程1");
Thread th2 = new Thread(run,"线程2");
Thread th3 = new Thread(run,"线程3");
th1.start();
th2.start();
th3.start();
}
}

wait方法,一旦执行,线程进入阻塞状态,并释放对象的锁。

notify方法,一旦执行,会唤醒等待的线程,如果有多个线程,唤醒优先级高的

notifyAll方法,会 唤醒所有等待的线程。

这三个方便必须再同步方法或同步代码块中使用。

sleep和wait方法的异同?相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。

不同点:两个方法声明的位置不同,Thread中是sleep方法。Object类中是wait方法。

调用的范围不同:sleep方法可以再任何需要的场景下调用。wait必须在同步代码块中运行。如果两个方 法都用在同步代码块或同步方法中,sleep不会释放对象锁。wait方法会释放锁。

生产者与消费者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 工厂
* @author Administrator
*
*/
public class Factory {
int[] arr = new int[1];//生产者生产的物品放入该数组中,消费者从该数组中取值
int index = 0;//记录生产者生产一个,或消费者消费一个
//1.生产东西
public synchronized void setProduct(int num) {
try {
if(index==1) {//生产者已经生产,但是消费者未消费
this.wait(3000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
arr[0]=num;//生产的东西保存,以便消费者消费
System.out.println("生产者生产:"+arr[0]);
this.notify();//通知消费者消费
index++;
}
//2.取东西
public synchronized void getProduct() {
try {
if(index==0) {
this.wait();//如果发现index==0说明还未生产好,需要等待
}
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("消费者消费:"+arr[0]);
index--;
//this.notify();//通知生产者生产
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Product extends Thread{
Factory factory = null;
public Product(Factory factory) {
super();
this.factory = factory;
}

@Override
public void run() {
for(int i=1;i<=10;i++) {
factory.setProduct(i);
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Customer extends Thread {
Factory factory = null;
public Customer(Factory factory) {
super();
this.factory = factory;
}

@Override
public void run() {
for(int i=1;i<=10;i++) {
factory.getProduct();
}
}
}

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
Factory fa = new Factory();
Product p = new Product(fa);
p.start();
Customer c = new Customer(fa);
c.start();
}
}

6、其他创建线程方式

1> 实现Callable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ThreadNewTest {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();

try {
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

2>线程池:

经常创建和销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响很大。如何 解决?提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁的创建 销毁,实现重复利用。使用线程池的 好处:提高响应速度,减少了创建新线程的时间,奖励资源消耗, 重复利用线程池中的线程,不需要每次都创建。便于线程管理。有一些属性:corePoolSize:核心池的大 小,maxnumPoolSize:最大线程数,keeAliveTime:线程没有任务时最多保持多长时间会终止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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);
}
}
}
}

public class ThreadPool {

public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池的属性
// System.out.println(service.getClass());
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();

//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable

// service.submit(Callable callable);//适合使用于Callable
//3.关闭连接池
service.shutdown();
}
}

十四、枚举和注解

1、枚举

当需要定义一组常量时,强烈建议使用枚举类

枚举类的实现 JDK1.5之前需要自定义枚举类 JDK 1.5 新增的 enum 关键字用于定义枚举类

若枚举只有一个对象, 则可以作为一种单例模式的实现方式。

枚举类的属性

枚举类对象的属性不应允许被改动, 所以应该使用 private final 修饰

枚举类的使用 private final 修饰的属性应该在构造器中为其赋值若枚举类显式的定义了带参数的构造器, 则在列出枚举值时也必须对应的传入参数。

1
2
3
4
enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum SeasonEnum {
SPRING("Spring","春意盎然"),
SUMMER("Summer","烈日炎炎"),
AUTUMN("Autumn","秋高气爽"),
WINTER("Winter","寒风凛冽");
private final String seasonName;
private final String seasonDesc;
private SeasonEnum(String seasonName, String seasonDesc) {
this.seasonName = seasonName;
this.seasonDesc = seasonDesc; }
public String getSeasonName() {
return seasonName; }
public String getSeasonDesc() {
return seasonDesc; } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public enum SeansEnum implements info{
SPRING("Spring","春意盎然"){
public void show(){
System.out.println("春天,春意盎然");
}
},
SUMMER("Summer","烈日炎炎"){
public void show(){
System.out.println("夏天,夏日炎炎");
}
},
AUTUMN("Autumn","秋高气爽"){
public void show(){
System.out.println("秋天,秋高气爽");
}
},
WINTER("Winter","寒风凛冽"){
public void show(){
System.out.println("冬天,冰雪皑皑");
}
};
private final String seasonName;
private final String seasonDesc;
private SeansEnum(String seasonName, String seasonDesc) {
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
}

interface info{
public void show();
}

class Test{
public static void main(String[] args) {
SeansEnum autumn = SeansEnum.AUTUMN;
autumn.show();
}
}

2、注解

从 JDK 5.0 开始, Java 增加了对元数据(MetaData) 的支持, 也就是Annotation(注解)

Annotation 其实就是代码里的特殊标记, 这些标记可以在编译, 类加载, 运行时被读取, 并执行相应的处 理。通过使用 Annotation, 程序员可以在不改变原有逻辑的情况下, 在源文件中嵌入一些补充信息。代码 分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者进行部署。

Annotation 可以像修饰符一样被使用, 可用于修饰包,类, 构造器,方法,成员变量, 参数,局部变量的声明, 这 些信息被保存在 Annotation 的 “name=value” 对中

常见注解

1
2
3
4
@Override: 限定重写父类方法, 该注解只能用于方法
@Deprecated: 用于表示所修饰的元素(类, 方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择
@SuppressWarnings: 抑制编译器警告

JDK中的元注解

JDK 的元 Annotation 用于修饰其他 Annotation 定义。

JDK5.0提供了4个标准的meta-annotation类型,分别是:Retention Target Documented Inherited。

@Retention: 只能用于修饰一个 Annotation 定义, 用于指定该 Annotation 的生命周期, @Rentention 包含一个 RetentionPolicy 类型的成员变量, 使用@Rentention 时必须为该 value 成员变量指定值:

**RetentionPolicy.SOURCE:**在源文件中有效(即源文件保留),编译器直接丢弃这种策略的注释

**RetentionPolicy.CLASS:**在class文件中有效(即class保留) , 当运行 Java 程序时, JVM 不会保留注 解。 这是默认值

RetentionPolicy.RUNTIME:在运行时有效(即运行时保留),当运行 Java 程序时, JVM 会保留注释。 程序可以通过反射获取该注释。

@Target: 用于修饰 Annotation 定义, 用于指定被修饰的 Annotation 能用于修饰哪些程序元素。

@Target 也包含一个名为 value 的成员变量。

十五、集合

数组在内存存储方面的特点:数组初始化以后,长度就确定了。数组声明的类型,就决定了进行元素初 始化时的类型数组在存储数据方面的弊端:数组初始化以后,长度就不可变了,不便于扩展。数组中提 供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。同时无法直接获取存储元素的 个数。数组存储的数据是有序的、可以重复的。—->存储数据的特点单一,Java 集合类可以用于存储数 量不等的多个对象,还可用于保存具有映射关系的关联数组。保存数据的时候,需要考虑使用集合。

Java 集合可分为 Collection 和 Map 两种体系。Collection接口:单列数据,定义了存取一组对象的方法 的集合List:元素有序、可重复的集合。Set:元素无序、不可重复的集合。Map接口:双列数据,保存 具有映射系“key-value对”的集合。

image-20220220202502317

image-20220220202508889

1、Collection接口

既然Collection接口是集合中的顶层接口,那么它中定义的所有功能子类都可以使用。查阅API中描述的 Collection接口。Collection 层次结构 中的根接口。Collection 表示一组对象,这些对象也称为 collection 的元素。一些 collection 允许有重复的元素,而另一些则不允许。一些 collection 是有序 的,而另一些则是无序的。这里我们不关心具体创建的Collection中的那个子类对象,这里重点演示的是 Collection接口中的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
boolean add(E e):确保此 collection 包含指定的元素(可选操作)。
boolean addAll(Collection<? extends E> c):将指定 collection 中的所有元素都添加到此collection 中(可选操作)。
void clear():移除此 collection 中的所有元素(可选操作)。
boolean contains(Object o):如果此 collection 包含指定的元素,则返回 true
boolean containsAll(Collection<?> c):如果此 collection 包含指定 collection 中的所有元素,则返回 true
boolean equals(Object o):比较此 collection 与指定对象是否相等。
int hashCode():返回此 collection 的哈希码值。
boolean isEmpty():如果此 collection 不包含元素,则返回 true
Iterator iterator():返回在此 collection 的元素上进行迭代的迭代器。
boolean remove(Object o):从此 collection 中移除指定元素的单个实例,如果存在的话(可选操作)。
boolean removeAll(Collection<?> c):移除此 collection 中那些也包含在指定collection 中的所有元素(可选操作)。
boolean retainAll(Collection<?> c):仅保留此 collection 中那些也包含在指定collection 的元素(可选操作)。
int size():返回此 collection 中的元素数。
Object[] toArray():返回包含此 collection 中所有元素的数组。
T[] toArray(T[] a):返回包含此 collection 中所有元素的数组;返回数组的运行时类型与指定数组的运行时类型相同。

2、迭代器的使用

Iterator原理由于集合容器有很多,每个容器都有自身的数据存储结构,即每个容器自身最清楚自己中数 据是如何存储的,容器这么多,每个容器数据存储又不相同,这时就在它们之间找取出元素的共性进行 了抽取,抽取出集合容器取出元素的共同特点。在取元素之前先要判断集合中有没有元素,如果有,就 把这个元素取出来,继续在判断,如果还有就再取出来。一直把集合中的所有元素全部取出。这种取出 方式专业术语称为迭代。集合中把这种取元素的方式描述在Iterator接口中。

注意:在进行集合元素取出时,如果集合中已经没有元素了,还继续使用迭代器的next方法,将会发生 java.util.NoSuchElementException没有这个元素异常。

3、List接口常用接口

鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组。一个有序集合(也被称为序列)。 此接口的用户在列表中的每个元素都被插入的地方有精确的控制。用户可以通过它们的整数索引(在列 表中的位置)访问元素,并在列表中搜索元素。 与set不同的是,列表通常允许重复元素。

List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。

List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。

JDK API中List接口的实现类常用的有:ArrayList、LinkedList和Vector。

List方法

1
2
3
4
5
6
7
8
9
10
11
void add(int index, E element) :在列表的指定位置插入指定元素(可选操作)。
boolean addAll(int index, Collection<? extends E> c) :将指定collection 中的所有元素都插入到列表中的指定位置。
E get(int index) :返回列表中指定位置的元素。
int indexOf(Object o) :返回此列表中第一次出现的指定元素的索引;如果此列表不包含该元素,则返回 -1
int lastIndexOf(Object o) :返回此列表中最后出现的指定元素的索引;如果列表不包含此元素,则返回 -1
ListIterator listIterator() :返回此列表元素的列表迭代器(按适当顺序)。
E remove(int index) :移除列表中指定位置的元素(可选操作)。
E set(int index, E element) :用指定元素替换列表中指定位置的元素(可选操作)。
List subList(int fromIndex, int toIndex) :返回列表中指定的 fromIndex (包括 )和toIndex (不包括)之间的部分视图。
default void sort(Comparator<? super E> c) :分类列表使用提供的 Comparator比较元素。

List Iterator介绍

在迭代过程中,使用了集合的方法对元素进行操作。导致迭代器并不知道集合中的变化,容易引发数据 的不确定性。解决:在迭代时,不要使用集合的方法操作元素。那么想要在迭代时对元素操作咋办?可 以使用迭代器的方法操作。可是很遗憾:迭代器Iterator的方式只有 hasNext() ,next(),remove();Iterator 有一个子接ListIterator可以完成该问题的解决。如何获取该子接口对象呢?通过List接口中的 listIterator()就可以获取。

List 实现子类

Collection接口:单列集合,用来存储一个一个的对象。

List接口:存储有序的、可重复的数据。 –>“动态”数组,替换原有的数组。

ArrayList:作为List接口的主要实现类;线程不安全的,效率高;底层使用Object[] elem。entData存 储

LinkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储。

Vector:作为List接口的古老实现类,线程安全的,效率低。底层使用Object[] elementData存储。

ArrayList List 接口的大小可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元 素。

每个 ArrayList 实例都有一个容量 。该容量是指用来存储列表元素的数组的大小。它总是至少等于列表 的大小。随着向 ArrayList 中不断添加元素,其容量也自动增长。此实现不是同步的。

ArrayList的JDK1.8之前与之后的实现区别?

JDK1.7:ArrayList像饿汉式,直接创建一个初始容量为10的数组

1
2
3
4
5
6
7
8
9
10
11
private transient Object[] elementData;
public ArrayList() {
this(10);
}
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementData

list.add();/如果此次的添加导致底层elementData数组容量不够,则扩容。

默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。

结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)

JDK1.8:ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创建一个始容量 为10的数组。默认扩容是原来的1.5倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

LinkedList List 接口的链接列表实现。实现所有可选的列表操作,并且允许所有元素(包括 null )。 除了实现 List 接口外, LinkedList 类还为在列表的开头及结尾 get 、 remove 和 insert 元素提供了统一 的命名方法。这些操作允许将链接列表用作堆栈、队列双端队列。所有操作都是按照双重链接列表的 需要执行的。在列表中编索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。

LinkedList list = new LinkedList(); 内部声明了Node类型的first和last属性,默认值为null。

list.add(123);//将123封装到Node中,创建了Node对象。

Node定义为:体现了LinkedList的双向链表的说法

1
2
3
4
5
6
7
8
9
10
11
12
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;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
void addFirst(E e):将指定元素插入此列表的开头。
void addLast(E e):将指定元素添加到此列表的结尾。
E removeFirst():移除并返回此列表的第一个元素。
E removeLast():移除并返回此列表的最后一个元素。
E getFirst():返回此列表的第一个元素。
E getLast():返回此列表的最后一个元素。
boolean ow erFirst(E e):在此列表的开头插入指定的元素。
boolean ow erLast(E e):在此列表的末尾插入指定的元素。
E pollFirst():获取并移除此列表的第一个元素;如果此列表为空,则返回 null 。 E
pollLast() :获取并移除此列表的最后一个元素;如果此列表为空,则返回 null 。 E
peekFirst() :获取但不移除此列表的第一个元素;如果此列表为空,则返回 null 。 E
peekLast() :获取但不移除此列表的最后一个元素;如果此列表为空,则返回 null

Vector 是一个古老的集合,JDK1.0就有了。大多数操作与ArrayList相同,区别之处在于Vector是线程 安全的。

在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList;Vector总是比 ArrayList慢,所以尽量避免使用。

请问ArrayList/LinkedList/Vector的异同?谈谈你的理解?ArrayList底层是什么?扩容机制?Vector和 ArrayList的最大区别?

1
2
3
4
5
6
ArrayList和LinkedList的异同
二者都线程不安全,相对线程安全的Vector,执行效率高。
此外,ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。
ArrayList和Vector的区别
Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大小的2倍空间而ArrayList是1.5倍。Vector还有一个子类Stack。

List接口的常用方法:

1
2
3
4
5
6
7
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
list.remove(2);//索引
list.remove(new Integer(2));//2这个值

小结:

Collection接口:单列集合,用来存储一个一个的对象

List接口:存储有序的、可重复的数据。 –>“动态**”数组,替换原有的数组

ArrayList:作为List接口的主要实现类;线程不安全的,效率高;底层使用Object[ ] elementData存 储

LinkedList:对于频繁的插入、删除操作,使用此类效率较高;底层使用双向链表存储

Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[ ] elementData存储 ArrayList list = new ArrayList(); 底层在创建对象时就创建了长度是10的Object[ ]数组elementData list.add(123);//elementData[0] = new Integer(123); …

list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。

默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。

结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)

2.2 jdk 8中ArrayList的变化:

ArrayList list = new ArrayList();//底层Object[ ] elementData初始化为{}.并没有创建长度为10的数组 源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
transient Object[] elementData; // non-private to simplify nested class	access
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 如果是无参构造器时,实际也创建了对象,就是没有长度而已
* 此时只是private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
* 赋值给了Object数组
* 当在第一次调用add方法添加元素时确定了数组长度
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 当为有参构造器时
* 参数为想要创建数组的长度
* 此时将会创建一个数组长度为传进来的长度
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
1
2
3
4
5
6
7
8
9
10
11
list.add(123);//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到	elementData[0]
/**
* 此时数组在第一次添加元素时,而是通过扩容机制来给数组确定长度,而第一次给数组
* 扩容的大小为10
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

image-20220314205442157

后续的添加和扩容操作与jdk 7 无异。

image-20220315173400374

小结:jdk7中的ArrayList的对象的创建类似于单例的饿汉式(以上来就创建),而jdk8中的ArrayList的 对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。

LinkedList源码分析

LinkedList list = new LinkedList(); 内部声明了Node类型的first和last属性,默认值为null

list.add(123);//将123封装到Node中,创建了Node对象。

其中,Node定义为:体现了LinkedList的双向链表的说法

1
2
3
4
5
6
7
8
9
10
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的源码分析:jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。

在扩容方面,默认扩容为原来的数组长度的2倍。

源码分析:

image-20220314205635875

1
2
3
4
5
6
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
list.remove(2);//2 是索引
list.remove(new Integer(2));//2是值

4、Set接口

Set接口是Collection的子接口,set接口没有提供额外的方法 Set 集合不允许包含相同的元素,如果试把 两个相同的元素加入同一个Set 集合中,则添加操作失败。Set 判断两个对象是否相同不是使用 == 运算 符,而是根据 equals() 方法。

HashSet:HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。

HashSet 具有以下特点:不能保证元素的排列顺序。HashSet 不是线程安全的。集合元素可以是 null

HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。 对于存放在Set容器中的对象,对应的类一定要重写equals()和 hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。

向HashSet中添加元素的过程:当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据 hashCode 值,通过某种散列函数决定该对 象在 HashSet 底层数组中的存储位置。(这个散列函数会与底层数组的长度相计算得到在数组中的下 标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布,该散列函数设计的越好)

如果两个元素的hashCode()值相等,会再继续调用equals方法,如果equals方法结果为true,添加失 败;如果为false,那么会保存该元素,但是该数组的位置已经有元素了,那么会通过链表的方式继续链 接。

如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相等,hashSet 将会把它们存 储在不同的位置,但依然可以添加成功。

HashSet底层也是数组,初始容量为16,当如果使用率超过0.75,(16*0.75=12)就会扩大容量为原来 的2倍。(16扩容为32,依次为64,128….等)。

重写equals方法的基本原则:

当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写hashCode(),根据一个类的 equals方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据 Object.hashCode()方法,它们仅仅是两个对象。违反了“相等的对象必须具有相等的散列码”。所以 复写 equals方法的时候一般都需要同时复写hashCode方法。通常参与计算hashCode的对象的属性也应该参 与到equals()中进行计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class Person {
private Integer pid;
private String pname;
private String psex;
private Integer page;
......
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
if (pid != null ? !pid.equals(person.pid) : person.pid != null)return false;
if (pname != null ? !pname.equals(person.pname) : person.pname !=
null) return false;
if (psex != null ? !psex.equals(person.psex) : person.psex != null)
return false;
return page != null ? page.equals(person.page) : person.page ==
null;
}
@Override
public int hashCode() {
int result = pid != null ? pid.hashCode() : 0;
result = 31 * result + (pname != null ? pname.hashCode() : 0);
result = 31 * result + (psex != null ? psex.hashCode() : 0);
result = 31 * result + (page != null ? page.hashCode() : 0);
return result;
}
}
public static void main(String[] args) {
Person p1 = new Person(1001,"c","f",23);
Person p2 = new Person(1000,"b","m",24);
Person p3 = new Person(1004,"a","f",18);
Person p4 = new Person(1003,"e","m",27);
Person p5 = new Person(1003,"e","m",27);
System.out.println(p4);
System.out.println(p5);
HashSet<Person> hashSet = new HashSet<>();
hashSet.add(p1);
hashSet.add(p2);
hashSet.add(p3);
hashSet.add(p4);
p4.setPid(1111);
System.out.println(p4);
hashSet.add(p5);
System.out.println("------------------");
System.out.println(p4);
System.out.println(p5);
hashSet.add(p4);
System.out.println(hashSet.size());//6
System.out.println(p4.equals(p5));
}

image-20220314220431423

LinkedHashSet:LinkedHashSet 是 HashSet 的子类。LinkedHashSet 根据元素的 hashCode 值来决 定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。

LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。

LinkedHashSet 不允许集合元素重复。

TreeSet:TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。TreeSet底层 使用红黑树结构存储数据。

排序:自然排序:TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元 素之间的大小关系,然后将集合元素按升序(默认情况)排列如果试图把一个对象添加到 TreeSet 时,则 该对象的类必须实现 Comparable 接口。

实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小。

Comparable 的典型实现:BigDecimal、BigInteger 以及所有的数值型对应的包装类:按它们对应的数 值大小进行比较。Character:按字符的 unicode值来进行比较。Boolean:true 对应的包装类实例大于 false 对应的包装类实例。String:按字符串中字符的 unicode 值进行比较。Date、Time:后边的时 间、日期比前面的时间、日期大。

5、Map接口

Map与Collection并列存在。用于保存具有映射关系的数据:key-value。Map 中的 key 和 value 都可以 是任何引用类型的数据。Map 中的 key 用Set来存放,不允许重复,即同一个 Map 对象所对应的类,须 重写hashCode()和equals()方法。常用String类作为Map的“键”。key 和 value 之间存在单向一对一关 系,即通过指定的 key 总能找到唯一的确定的 value。Map接口的常用实现类:HashMap、TreeMap、 LinkedHashMap和Properties。其中,HashMap是 Map 接口使用频率最高的实现类。

Map接口常用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
添加、删除、修改操作:
Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中void putAll(Map m):将m中的所有key-value对存放到当前map中
Object remove(Object key):移除指定key的key-value对,并返回value
void clear():清空当前map中的所有数据

元素查询的操作:
Object get(Object key):获取指定key对应的value
boolean containsKey(Object key):是否包含指定的key
boolean containsValue(Object value):是否包含指定的value
int size():返回map中key-value对的个数
boolean isEmpty():判断当前map是否为空
boolean equals(Object obj):判断当前map和参数对象obj是否相等

元视图操作的方法:
Set keySet():返回所有key构成的Set集合
Collection values():返回所有value构成的Collection集合
Set entrySet():返回所有key-value对构成的Set集合

HashMap:HashMap是 Map 接口使用频率最高的实现类。允许使用null键和null值,与HashSet一样, 不保证映射的顺序。所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写: equals()和hashCode()所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的 类要重写:equals()一个key-value构成一个entry。所有的entry构成的集合是Set:无序的、不可重复 的。HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相 等。HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。

JDK 7及以前版本:HashMap是数组+链表结构(即为链地址法)

JDK 8版本发布以后:HashMap是数组+链表+红黑树实现。

1
2
3
4
HashMap的内部存储结构其实是数组和链表的结合。当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。而且新添加的元素作为链表的head。
添加元素的过程:向HashMap中添加entry1(key,value),需要首先计算entry1中key的哈希值(根据key所在类的hashCode()计算得到),此哈希值经过处理以后,得到在底层Entry[]数组中要存储的位置i。如果位置i上没有元素,则entry1直接添加成功。如果位置i上已经存在entry2(或还有链表存在的entry3,entry4),则需要通过循环的方法,依次比较entry1中key和其他的entry。如果彼此hash值不同,则直接添加成功。如果hash值不同,继续比较二者是否equals。如果返回值为true,则使用entry1的value去替换equals为true的entry的value。如果遍历一遍以后,发现所有的equals返回都为false,则entry1仍可添加成功。entry1指向原有的entry元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
	JDK1.8之前
HashMap的扩容
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算
其在新数组中的位置,并放进去,这就是resize。那么HashMap什么时候进行扩容呢? 当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)*loadFactor 时 , 就 会 进行 数 组 扩 容 , loadFactor 的默认 值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数
超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
JDK1.8之后
HashMap的内部存储结构其实是数组+链表+树的结合。当实例化一个HashMap时,会初始化
initialCapacity和loadFactor,在put第一对映射关系时,系统会创建一个长度为initialCapacity的Node数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。
那么HashMap什么时候进行扩容和树形化呢?
当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个size)*loadFactor 时, 就会进行数组扩容 ,loadFactor 的默认 值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。 当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。

总结:JDK1.8相较于之前的变化:
1.HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
2.当首次调用map.put()时,再创建长度为16的数组
3.数组为Node类型,在jdk7中称为Entry类型
4.形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
5.当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有keyvalue对使用红黑树进行存储

LinkedHashMap:LinkedHashMap 是 HashMap 的子类在HashMap存储结构的基础上,使用了一对 双向链表来记录添加元素的顺序与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代顺序: 迭代顺序与 Key-Value 对的插入顺序一致。

TreeMap: TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。TreeSet底层使用红黑树结构存储数据。TreeMap 的 Key 的排序:自然排 序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否 则将会抛出ClasssCastException。定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象 负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口TreeMap判断 两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。

**Hashtable:**Hashtable是个古老的 Map 实现类,JDK1.0就提供了。不同于HashMap,Hashtable是线 程安全的。Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很 多情况下可以互用。 与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value。与HashMap一 样,Hashtable 也不能保证其中 Key-Value 对的顺序。Hashtable判断两个key相等、两个value相等的 标准,与HashMap一致。 **Properties:**Properties 类是 Hashtable 的子类,该对象用于处理属性文件由于属性文件里的 key、 value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型存取数据时,建议使用 setProperty(String key,String value)方法和getProperty(String key)方法

总结:Map集合

一、Map的实现类的结构:

Map:双列数据,存储key-value对的数据 。

HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value

LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。

原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁 的遍历操作,此类执行效率高于HashMap。

源码分析

image-20220314220552497

该构造方法中,并没有立即创建Entry数组。

image-20220314220613212

调用put方法

image-20220314221430005

Properties:Properties 类是 Hashtable 的子类,该对象用于处理属性文件由于属性文件里的 key、 value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型存取数据时,建议使用 setProperty(String key,String value)方法和getProperty(String key)方法。

TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制 排序。底层使用红黑树。

代码实现排序:Comparator接口Comparable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 默认的从小到大
* 如果想进行从大到小则this.name.compareTo(user.name)
* 前加一个负号
* compareTo比较并不是equals
*
*
* Comparator接口下的方法是:(定制排序)
* int compare(T o1, T o2);
* Comparable接口下的方法是:(treeSet中默认的排序所实现的接口,也是自然排序)
* int compareTo(Object o)
*
* 共性:
* Integer.compare(user1.age, user2.age);这种方式适合比较数字
* 此方法是Comparator接口下的方法
* user1.compareTo(user2.name);这种适合比较字符
* 此方法是Comparable接口下的方法
*
*/
@Override
public int compareTo(Object o) {
int compare = 0;
Comparator<Object> comparator = new Comparator<Object>() {
//匿名实现
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof User && o2 instanceof User) {
User user1 = (User)o1;
User user2 = (User)o1;
// return Integer.compare(user1.age, user2.age);
return user1.compareTo(user2.name);
}else {
throw new RuntimeException();
}
}
};
if(o instanceof User) {
User user = (User)o;
compare = -this.name.compareTo(user.name);
if(compare == 0) {
// return this.age.compareTo(user.age);
//compare方法是Integer包装类自己定义的,并非Comparable接口下的方法
return Integer.compare(this.age, user.age);
}else {
return compare;
}
}else {
throw new RuntimeException();
}
}

6.、Collections工具类

Collections 是一个操作 Set、List 和 Map 等集合的工具类

Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,

还提供了对集合对象设置不可变、对集合对象实现同步控制等方法

排序操作:(均为static方法)

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

常用方法:

1
2
3
4
5
6
7
8
9
Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回
给定集合中的最大元素
Object min(Collection)
Object min(Collection,Comparator)
int frequency(Collection,Object):返回指定集合中指定元素的出现次数
void copy(List dest,List src):将src中的内容复制到dest中 boolean
replaceAll(List list,Object oldVal,Object newVal):使用新值替换
List 对象的所有旧值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

public class Person {
private Integer id;
private String name;
......
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
if (!id.equals(person.id)) return false;
return name.equals(person.name);
}
@Override
public int hashCode() {
int result = id.hashCode();
result = 31 * result + name.hashCode();
return result
}
}

public static void test01(){
HashSet set = new HashSet();
Person p1 = new Person(1001,"AA");
Person p2 = new Person(1002,"BB");
set.add(p1);
set.add(p2);
System.out.println(set);
p1.setName("CC"); set.remove(p1);
System.out.println(set);
set.add(new Person(1001,"CC"));
System.out.println(set);
set.add(new Person(1001,"AA"));
System.out.println(set);
}

练习:

1、集合Collection中存储的如果是自定义类的对象,需要自定义类重写哪个方法?为什么?

2、ArrayList,LinkedList,Vector三者的相同点和不同点

3、List接口的常用方法有哪些

4、Set存储数据的特点是什么?有哪些实现类,简述各实现类的特点

5、如何去除list中重复的数据

6、创建一个List集合的对象,添加几个数字,反转对象中元素的顺序;根据元素的自然顺序排序

7、一组数列A[25,36,17,23,14] ,一组数列B[10,8,25,36,14,72] 将两个数列合并,重复只出现一次, 并按照从小到打输出

8、随机生成五组双色球彩票,双色球规则,是这样的 红号是1-33 取六个不重复的,蓝号是1-16 取一个。

9、课堂演示是学生管理系统,分别采用三个集合实现

10、发的业务图片尝试实现[]

十六、泛型

集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在JDK1.5之前 只能把元素类型设计为Object,JDK1.5之后使用泛型来解决。因为这个时候除了元素的类型不确定,其 他的部分是确定的,例如关于这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成 一个参数,这个类型参数叫做泛型。Collection,List,ArrayList 这个就是类型参数,即泛型。

所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值 及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象 时)确定(即传入实际的类型参数,也称为类型实参)。

从JDK1.5以后,Java引入了“参数化类型(Parameterized type)”的概念,允许我们在创建集合时再指 定集合元素的类型,正如:List,这表明该List只能保存字符串类型的对象。 JDK1.5改写了集合框架中的 全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型 实参。

为什么要有泛型?

解决元素存储的安全性问题,解决获取数据元素时,需要类型强制转换的问题。

只有指定类型才可以添加到集合中:类型安全 读取出来的对象不需要强转:便捷

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

1、集合中使用泛型

2、自定义泛型:

泛型类:泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的 接口。最典型的就是各种容器类,如:List、Set、Map。

1
2
3
4
5
//泛型类的最基本写法(这么看可能会有点晕,会在下面的例子中详解):
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
private 泛型标识 /*(成员变量类型)*/ var; .....
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
//key这个成员变量的类型为T,T的类型由外部指定
private T key;
public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
this.key = key;
}
public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定 return key;
}
}
//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
//传入的实参类型需与泛型的类型参数类型相同,即为Integer.
Generic<Integer> genericInteger = new Generic<Integer>(123456); //传入的实参类型需与泛型的类型参数类型相同,即为String.
Generic<String> genericString = new Generic<String>("key_vlaue");

泛型接口:泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一 个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
/*** 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
即:class FruitGenerator<T> implements Generator<T>{ }
如果不声明泛型,如:
class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
*/
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*** 传入泛型实参时:
定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实
参类型
即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
*/
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}

泛型方法:

image-20220319095317417

十七、文件与IO

1、File类的使用

java.io.File类:文件和文件目录路径的抽象表示形式,与平台无关。

File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要访问文件内容本身, 则需要使用输入/输出流。

想要在Java程序中表示一个真实存在的文件或目录,那么必须有一个File对 象,但是Java程序中的一个 File对象,可能没有一个真实存在的文件或目录。

构造方法:

public File(String pathname) 以pathname为路径创建File对象,可以是绝对路径或者相对路径,如 果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储。 绝对路径:是一个固定的路 径,从盘符开始相对路径:是相对于某个位置开始

**public File(String parent,String child)**以parent为父路径,child为子路径创建File对象。

**public File(File parent,String child)**根据一个父File对象和子文件路径创建File对象。

常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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() :判断是否隐藏

**listFiles():**方法介绍文件都存放在目录(文件夹)中,那么如何获取一个目录中的所有文件或者目录中 的文件夹呢?那么我们先想想,一个目录中可能有多个文件或者文件夹,那么如果File中有功能获取到一 个目录中的所有文件和文件夹,那么功能得到的结果要么是数组,要么是集合。

**文件过滤器:**通过上述方法,我们可以获取到一个目录下的所有文件和文件夹,但能不能对其进行过滤 呢?比如我们只想要一个目录下的指定扩展名的文件,或者包含某些关键字的文件夹呢?我们是可以先 把一个目录下的所有文件和文件夹获取到,并遍历当前获取到所有内容,遍历过程中在进行筛选,但是 这个动作有点麻烦,Java给我们提供相应的功能来解决这个问题。查阅File类的API,在查阅时发现File类 中重载的listFiles方法,并且接受指定的过滤器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class FileDemo2 {
public static void main(String[] args) {
//获取扩展名为.java所有文件
//创建File对象
File file = new File("E:\\JavaSE1115\\code\\day11_code");
//获取指定扩展名的文件,由于要对所有文件进行扩展名筛选,因此调用方法需要传递过滤器
File[] files = file.listFiles(new FileFilterBySuffix()); //遍历获取到的所有符合条件的文件
for (File f : files) {
System.out.println(f);
}
}
}
//定义类实现文件名称FilenameFilter过滤器
class FileFilterBySuffix implements FilenameFilter{
public boolean accept(File dir, String name) {
return name.endsWith(".java");
}
}

class FileFilterBySuffix implements FileFilter{
private String suffix;
public FileFilterBySuffix(String suffix) {
super();
this.suffix = suffix;
}
@Override
public boolean accept(File pathname) {
return pathname.getName().endsWith(suffix);
}
}

2、IO流

I/O是Input/Output的缩写, I/O技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文 件,网络通讯等。

Java程序中,对于数据的输入/输出操作以“流(stream)”的方式进行。

IO原理:输入input:读取外部数据到程序(内存)中

​ 输出output:将程序(内存)数据输出到磁盘、光盘等存储社保中。

流的分类:按照数据单位来分:字节流,字符流

​ 按照流向来分:输入流,输出流。

image-20220220202806515

image-20220220202815280

流的体系:

image-20220220202827677

3、InputStream和Reader

InputStream和Reader是所有输入流的基类

InputStream典型实现是FileInputStream。常用方法read()方法【int read(); int read(byte[] b) ;int read(byte[] b, int off, int len) 】

Reader典型实现是FileReader。常用方法read()方法【int read(); int read(byte[] b) ;int read(byte[] b, int off, int len) 】

程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件 IO 资源。

FileInputStream 从文件系统中的某个文件中获得输入字节。FileInputStream 用于读取非文本数据之类 的原始字节流。要读取字符流,需要使用 FileReader。

InputStream方法介绍:

int read():从输入流中读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。如果因为已经到 达流末尾而没有可用的字节,则返回值 -1。

int read(byte[] b):从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。如果因为已经 到达流末尾而没有可用的字节,则返回值 -1。否则以整数形式返回实际读取的字节数。

int read(byte[] b, int off,int len):将输入流中最多 len 个数据字节读入 byte 数组。尝试读取 len 个 字节,但读取的字节也可能小于该值。以整数形式返回实际读取的字节数。如果因为流位于文件末尾而 没有可用的字节,则返回值 -1。

public void close() throws IOException关闭此输入流并释放与该流关联的所有系统资源。

Reader方法介绍:

**int read()**读取单个字符。作为整数读取的字符,范围在 0 到 65535 之间 (0x00-0xffff)(2个字节的 Unicode码),如果已到达流的末尾,则返回 -1

**int read(char[] cbuf)**将字符读入数组。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符 数。

**int read(char[] cbuf,int off,int len)**将字符读入数组的某一部分。存到数组cbuf中,从off处开始存 储,最多读len个字符。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。

public void close() throws IOException关闭此输入流并释放与该流关联的所有系统资源。

4、OutputStream和Writer

OutputStream 和 Writer 也非常相似:

void write(int b/int c);

void write(byte[] b/char[] cbuf);

void write(byte[] b/char[] buff, int off, int len);

void flush();

void close(); 需要先刷新,再关闭此流。

因为字符流直接以字符作为操作单位,所以 Writer 可以用字符串来替换字符数组,即以 String 对象作 为参数

void write(String str);

void write(String str, int off, int len);

FileOutputStream 从文件系统中的某个文件中获得输出字节。FileOutputStream 用于写出非文本数据 之类的原始字节流。要写出字符流,需要使用 FileWriter。

练习:

将学生对象(姓名,语文分数,数学分数,英语分数,总分)按照总分从高到低排序,并将姓名和从 高到低总分写入文件中。

5、缓冲流

为了提高数据读写的速度,Java API提供了带缓冲功能的流类,在使用这些流类时,会创建一个内部缓 冲区数组,缺省使用8192个字节(8Kb)的缓冲区。

缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为:

BufferedInputStream 和 BufferedOutputStream

BufferedReader 和 BufferedWriter

当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区

当使用BufferedInputStream读取字节文件时,BufferedInputStream会一次性从文件中读取8192个 (8Kb),存在缓冲区中,直到缓冲区装满了,才重新从文件中读取下一个8192个字节数组。

向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满,BufferedOutputStream才 会把缓冲区中的数据一次性写到文件里。使用方法flush()可以强制将缓冲区的内容全部写入输出流

关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可,关闭最外层流也会相应关闭内层节点流

flush()方法的使用:手动将buffer中内容写入文件

如果是带缓冲区的流对象的close()方法,不但会关闭流,还会在关闭流之前刷新缓冲区,关闭后不能再 写出

6、转换流

转换流提供了在字节流和字符流之间的转换。

Java API提供了两个转换流:

InputStreamReader:将InputStream转换为Reader

OutputStreamWriter:将Writer转换为OutputStream

字节流中的数据都是字符时,转成字符流操作更高效。

很多时候我们使用转换流来处理文件乱码问题。实现编码和解码的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
InputStream in = new FileInputStream("D:\\test\\cn8.txt");
//创建转换流对象
//InputStreamReader isr = new InputStreamReader(in);这样创建对象,会用本地默认码
表 读取,将会发生错误解码的错误
InputStreamReader isr = new InputStreamReader(in,"utf-8");
//使用转换流去读字节流中的字节
int ch = 0;
while((ch = isr.read())!=-1){
System.out.println((char)ch);
}
//关闭流
isr.close();

1
2
3
4
5
6
7
8
9
//创建与文件关联的字节输出流对象
FileOutputStream fos = new FileOutputStream("D:\\test\\cn8.txt");
//创建可以把字符转成字节的转换流对象,并指定编码
OutputStreamWriter osw = new OutputStreamWriter(fos,"utf-8");
//调用转换流,把文字写出去,其实是写到转换流的缓冲区中
osw.write("你好");
//写入缓冲区。
osw.close();

字符编码:常见的编码表

ASCII:美国标准信息交换码。用一个字节的7位可以表示。

ISO8859-1:拉丁码表。欧洲码表用一个字节的8位表示。

GB2312:中国的中文编码表。最多两个字节编码所有字符

GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码

Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码。所有的文字 都用两个字节来表示。

UTF-8:变长的编码方式,可用1-4个字节来表示一个字符

7、标准输入、输出流

System.in和System.out分别代表了系统标准的输入和输出设备

默认输入设备是:键盘,输出设备是:显示器

System.in的类型是InputStream

System.out的类型是PrintStream,其是OutputStream的子类

FilterOutputStream 的子类

重定向:通过System类的setIn,setOut方法对默认设备进行改变。

public static void setIn(InputStream in)

public static void setOut(PrintStream out)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String s = null;
try {
while((s=br.readLine())!=null){
if("e".equalsIgnoreCase(s)||"exit".equalsIgnoreCase(s)){
System.out.println("安全退出");
break;
}
System.out.println("--->"+s.toUpperCase());
System.out.println("继续输入信息");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(br!=null){
br.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PrintStream ps = null;
try {
FileOutputStream out = new FileOutputStream(new
File("D:/out.txt"));
ps = new PrintStream(out,true);
if(ps!=null){
System.setOut(ps);
}
for(int i=65;i<=91;i++){
System.out.print((char)i);
if(i%50==0){
System.out.println();
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally{
if(ps!=null){
ps.close();
}
}

8、对象流

ObjectInputStream和OjbectOutputSteam:用于存储和读取基本数据类型数据或对象的处理流。它的强 大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。

序列化:用ObjectOutputStream类保存基本类型数据或对象的机制。

反序列化:用ObjectInputStream类读取基本类型数据或对象的机制。

对象的序列化:对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种 二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。//当其它程序获取 了这种二进制流,就可以恢复成原来的Java对象。序列化的好处在于可将任何实现了Serializable接口的 对象转换为字节数据,使其在保存和传输时可被还原。序列化是RMI(Remote Method Invoke-远程方法 调用)过程的参数和返回值都必须实现的机制。而RMI是javaEE的基础,因此序列化机制是JavaEE平台的 基础。如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某 个类是可序列化的,该类必须实现如下两个接口之一,否则会抛出NotSerializableException。通常实现 Serializable

凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:

private static final long serialVersionUID;

serialVersionUID用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象进行版本控制, 有关各版本反序列化时是否兼容。

如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节自动生成的。若类的实 例变量做了修改,serialVersionUID 可能发生变化。故建议,显式声明。

简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行 反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比 较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。 (InvalidCastException)。

若某个类实现了 Serializable 接口,该类的对象就是可序列化的:

创建一个 ObjectOutputStream

调用 ObjectOutputStream 对象的 writeObject(对象) 方法输出可序列化对象

注意写出一次,操作flush()一次。

反序列化

创建一个 ObjectInputStream

调用 readObject() 方法读取流中的对象

强调:如果某个类的属性不是基本数据类型或 String 类型,而是另一个引用类型,那么这个引用类型必 须是可序列化的,否则拥有该类型的Field 的类也不能序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
///序列化:将对象写入到磁盘或者进行网络传输
ObjectOutputStream oos = new ObjectOutputStream(new
FileOutputStream(“data.txt"));
Person p = new Person("张三", 18, "太白小区");
oos.writeObject(p);
oos.flush();
oos.close();
//反序列化:将磁盘中的对象数据源读出.
ObjectInputStream ois = new ObjectInputStream(new
FileInputStream(“data.txt"));
Person p1 = (Person)ois.readObject();
System.out.println(p1.toString());
ois.close();

谈谈你对Serializable接口的理解

1
2
实现了Serializable接口的对象,可将它们转换成一系列字节,并可在以后完全恢复回原来的样子。这一过程亦可通过网络进行。这意味着序列化机制能自动补偿操作系统间的差异。换句话说,可以先在Windows机器上创建一个对象,对其序列化,然后通过网络发给一台Unix机器,然后在那里准确无误地重新“装配”。不必关心数据在不同机器上如何表示,也不必关心字节的顺序或者其他任何细节。
由于大部分作为参数的类如String、Integer等都实现了java.io.Serializable的接口,也可以利用多态的性质,作为参数使接口更灵活

瞬态关键字

当一个类的对象需要被序列化时,某些属性不需要被序列化,这时不需要序列化的属性可以使用关键字 transient修饰。只要被transient修饰了,序列化时这个属性就不会琲序列化了。同时静态修饰也不会被 序列化,因为序列化是把对象数据进行持久化存储,而静态的属于类加载时的数据,不会被序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Person implements Serializable {
/** 给类显示声明一个序列版本号。 */
private static final long serialVersionUID = 1L;
private static String name;
private transient/*瞬态*/ int age;
public Person() {
super();
}
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
......
}

十八、反射

Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任 何类的内部信息,并能直接操作任意对象的内部属性及方法。

加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这 个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子, 透过这个镜子看到类的结构,所以,我们形象的称之为:反射。

动态语言

是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以 被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。主要 动态语言:Object-C、C#、JavaScript、PHP、Python、Erlang。

静态语言与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java、C、C++。

Java不是动态语言,但Java可以称之为“准动态语言”。即Java有一定的动态性,我们可以利用反射机制、 字节码操作获得类似动态语言的特性。Java的动态性让编程的时候更加灵活!

Java反射机制提供的功能:

在运行时判断任意一个对象所属的类

在运行时构造任意一个类的对象

在运行时判断任意一个类所具有的成员变量和方法

在运行时获取泛型信息

在运行时调用任意一个对象的成员变量和方法

在运行时处理注解

生成动态代理

反射机制主要API

**java.lang.Class:**代表一个类

java.lang.reflect.Method:代表类的方法

**java.lang.reflect.Field:**代表类的成员变量

**java.lang.reflect.Constructor:**代表类的构造器

1、Class类

在Object类中定义了以下的方法,此方法将被所有子类继承

以上的方法返回值的类型是一个Class类,此类是Java反射的源头,实际上所谓反射从程序的运行结果来 看也很好理解,即:可以通过对象反射求出类的名称。

Class类:某个类的属性、方法和构造器、某个类到底实现了哪些接口。对于每个类而言,JRE 都为其保 留一个不变的 Class 类型的对象。一个 Class 对象包含了特定某个结构 (class/interface/enum/annotation/primitive type/void/[])的有关信息。

Class本身也是一个类。Class对象只能由系统建立对象。

一个加载的类再JVM中只会由一个Class实例。一个Class对象对应一个加载到JVM中的要给.class文件。

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

通过Class可以完整得到一个类中的所有被加载的结构。 Class类是Reflection的根源,针对任何你想动态加载,运行的类,必须先获取相应的Class对象。

获取Class的几种方法:

1>若已知具体的类,通过类的class属性获取,该方法最可靠,程序性能最高。

Class clazz = Person.class

2>若已知某个类的对象,调用该对象的getClass方法获取class对象

3>知道类的全名,通过 Class.forName();获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
类:获取类的方式 Class.forName("");
对象.getClass();
类.class
加载类只会加载一次
获取类后才能获取类内部信息,属性,方法,构造方法
获取类后,newInstance方法创建对象,该方法默认会找无参构造,类中必须包含一个无参构造。【】
有了类后需要获取属性Field类型的对象
clazz.getFields();public修饰的属性
对于属性的操作有哪些?赋值,取值
静态属性:可以通过类直接操作,直接获取值或直接赋值
非静态属性:依赖对象,通过对象. 给对象是属性赋值或取值
Dog dog = null;
dog.setXm();
getField("name"):通过属性名获取属性信息
field.set();
field.get();
field.setAccessible(true);可以访问私有化的属性

2、类的加载与ClassLoader的理解

当程序主动创建某个类时,如果该类还未被加载到内存中,则系统会通过以下三个步骤来对类进行初 始化。

1>类的加载:将类的class文件读入内存,并为之创建和一个java.lang.Class对象,该过程由类加载器完 成。

类的加载与ClassLoader的理解:加载:将class文件字节码内容加载到内存中,并将这些静态数据转换 成方法区的运行时数据,然后生成一个代表这个类的Class对象,作为方法区中类数据的访问入口(地址 引用)。所有需要访问和使用类数据只能通过这个Class对象,这个加载的过程需要类加载器参与。

2>类的链接:将类的二进制数据合并到jre中。

将java类的二进制代码合并到JVM的运行状态之中的过程。

验证:确保加载的类信息符号JVM规范,利润,已cafe开头,没有安全方面的问题。

准备:正式为类变量static分配内存并设置类变量默认初始值的阶段,这些内存都将再方法区中进行 分配。

解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程

3>类的初始化:jvm负责对类进行初始化

执行类构造器clinit()方法的过程,类构造器clinit()方法是由编译期自动收集类中所有类变量的赋值动 作和静态代码块中的语句合并产生的。类构造器是构造类信息,不说构造该类对象的构造器。

当初始化一个类的时候,如果发现其父类还没进行初始化,则需要先触发其父类的初始化。

虚拟机会保证一个类的clinit()方法在多线程环境中被正确加锁和同步。

ClassLoader:

https://zhuanlan.zhihu.com/p/44670213

3、创建运行时类的对象

有了Class对象,能做什么?

创建类的对象:调用Class对象的newInstance()方法

要 求: 1)类必须有一个无参数的构造器。2)类的构造器的访问权限需要足够。

4、获取运行时类的完整结构

通过反射获取运行时类的完整结构:Field、Method、Constructor、Superclass、Interface、 Annotation。

使用反射可以取得:

1.实现的全部接口

public Class[] getInterfaces() 确定此对象所表示的类或接口实现的接口。

2.所继承的父类

public Class getSuperclass()返回表示此 Class 所表示的实体(类、接口、基本类型)的父 类的Class

3.全部的构造器

public Constructor[] getConstructors()返回此 Class 对象所表示的类的所有public构造方法。

public Constructor[] getDeclaredConstructors()返回此 Class 对象表示的类声明的所有构造方法。

Constructor类中:取得修饰符: public int getModifiers();取得方法名称: public String getName();取得 参数的类型:public Class[] getParameterTypes();

4.全部的方法

public Method[] getDeclaredMethods()返回此Class对象所表示的类或接口的全部方法

public Method[] getMethods() 返回此Class对象所表示的类或接口的public的方法

Method类中:public Class getReturnType()取得全部的返回值

public Class[] getParameterTypes()取得全部的参数

public int getModifiers()取得修饰符

public Class[] getExceptionTypes()取得异常信息。

调用指定的方法

通过反射,调用类中的方法,通过Method类完成。步骤:

1>通过Class类的**getMethod(String name,Class…parameterTypes)**方法取得一个Method对象,并 设置此方法操作时所需要的参数类型。

2>之后使用**Object invoke(Object obj, Object[] args)**进行调用,并向方法中传递要设置的obj对象的 参数信息

Object invoke(Object obj, Object … args):

说明:

Object 对应原方法的返回值,若原方法无返回值,此时返回null

若原方法若为静态方法,此时形参Object obj可为null

若原方法形参列表为空,则Object[] args为null

若原方法声明为private,则需要在调用此invoke()方法前,显式调用方法对象的setAccessible(true)方 法,将可访问private的方法

5.全部的Field

public Field[] getFields() 返回此Class对象所表示的类或接口的public的Field。

public Field[] getDeclaredFields() 返回此Class对象所表示的类或接口的全部Field。

Field方法中:public int getModifiers() 以整数形式返回此Field的修饰符

public Class getType() 得到Field的属性类型

public String getName() 返回Field的名称。

调用指定属性

在反射机制中,可以直接通过Field类操作类中的属性,通过Field类提供的set()和get()方法就可以完成设 置和取得属性内容的操作。

public Field getField(String name) 返回此Class对象表示的类或接口的指定的public的Field。

public Field getDeclaredField(String name)返回此Class对象表示的类或接口的指定的Field。

在Field中:

public Object get(Object obj) 取得指定对象obj上此Field的属性内容

public void set(Object obj,Object value) 设置指定对象obj上此Field的属性内容

6.Annotation相关

get Annotation(Class annotationClass) getDeclaredAnnotations()

7.泛型相关

获取父类泛型类型:Type getGenericSuperclass()

泛型类型:ParameterizedType 获取实际的泛型类型参数数组:getActualTypeArguments()

8.类所在的包

Package getPackage()

5、动态代理

创建接口与接口的实现类

1
2
3
4
5
6
public interface UserDao {
public void save();
public int update(Users user);
public int test(int a,int b);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserDaoImpl1 implements UserDao{
public void save(){
System.out.println("userDao中实现类1的save");
}
public int update(Users user) {
System.out.println("dao实现类1执行了。。。"+user);
return 10;
}
public int test(int a, int b) {
System.out.println("dao实现类1执行了。。。。");
return a+b;
}
}

ii.创建InvocationHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyInvocationHandler implements InvocationHandler{
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("方法执行之前执行...."+method.getName()+"...."+ Arrays.toString(args));
Object res = method.invoke(target,args);
System.out.println("方法执行之后执行");
return res;
}
}

1
2
3
4
5
6
7
8
9
10
11
public class SpringTest {
@Test
public void testProxy(){
UserDaoImpl1 dao = new UserDaoImpl1();
Class[] classes = dao.getClass().getInterfaces();
UserDao proxy = (UserDao) Proxy.newProxyInstance(SpringTest.class.getClassLoader(),classes,new MyInvocationHandler(dao));
int r = proxy.test(10,20);
System.out.println(r);
}
}

十九、网络编程

Java是 Internet 上的语言,它从语言级上提供了对网络应用程序的支持,程序员能够很容易开发常见的 网络应用程序。

Java提供的网络类库,可以实现无缝的网络连接,联网的底层细节被隐藏在 Java 的本机安装系统里,由 JVM 进行控制。并且 Java 实现了一个跨平台的网络库,程序员面对的是一个统一的网络编程环境。

网络编程的目的就是指直接或间接地通过网络协议与其它计算机进行通讯。

网络编程中有两个主要的问题:

如何准确地定位网络上一台或多台主机

找到主机后如何可靠高效地进行数据传输

要想让处于网络中的主机互相通信,只是知道通信双方地址还是不够的,还必须遵循一定的规则。有两 套参考模型:

OSI参考模型:模型过于理想化,未能在因特网上进行广泛推广

TCP/IP参考模型(或TCP/IP协议):事实上的国际标准。

  1. TCP/IP协议

TCP/IP 以其两个主要协议:传输控制协议(TCP)和网络互联协议(IP)而得名,实际上是一组协议,包括多 个具有不同功能且互为关联的协议。

TCP/IP协议模型从更实用的角度出发,形成了高效的四层体系结构,即网络接口层、IP层、传输层和应 用层。下图表示了TCP/IP的分层结构和与OSI参考模型的对应关系。

image-20220323144147553

传输层协议中有两个非常重要的协议:

  • 传输控制协议TCP(Transmission Control Protocol)

  • 用户数据报协议UDP(User Datagram Protocol)。

传输控制协议TCP是面向连接的传输层协议。即应用进程(或程序)在使用TCP协议之前,必须先建立TCP 连接,在传输完毕后,释放已经建立的连接。利用TCP协议进行通信的两个应用进程,一个是服务器进 程。另一个是客户进程。

• 用户数据报协议UDP是面向无连接的运输层协议。即应用进程(或程序)在使用UDP协议之前,不必先建 立连接。自然,发送数据结束时也没有连接需要释放。因此,减少了开销和发送数据之前的时延。 Internet上的主机有两种方式表示地址:

域名:www. baidu.com

IP 地址:180.97.33.108(动态IP地址)

  1. InetAddress简介

InetAddress 类对象含有一个 Internet 主机地址的域名和IP地址:www.baidu.com/180.97.33.108。

域名容易记忆,当在连接网络时输入一个主机的域名后,域名服务器(DNS)负责将域名转化成IP地 址,这样才能和主机建立连接。

获取Internet上主机的地址:使用InetAddress类的静态方法:

getByName(String s):将一个域名或 IP 地址传递给该方法的参数,获得一个 InetAddress对象,该对 象含有主机地址的域名和IP地址,该对象用如下格式表示它包含的信息: www.baidu.com/180.97.33.108

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
try {
InetAddress inter = InetAddress.getByName("www.baidu.com");
String add = inter.getHostAddress();
System.out.println(add);
} catch (Exception e) {
e.printStackTrace();
}
}

端口号:标识正在计算机上运行的进程。公认端口:0-1023。注册端口:1024-49151 。

TCP程序设计:

客户端-服务器模型是最常见的网络应用程序模型。当我们上网时,我们所使用的浏览器(例如IE)就是一 个客户端软件,而提供网页的站点必需运行一个WEB服务器。

一般而言,主动发起通信的应用程序属于客户端。而服务器则是等待通信请求,当服务器收到客户端的 请求,执行需要的运算然后向客户端返回结果。

IP 地址标识 Internet 上的计算机,端口号标识正在计算机上运行的进程(程序)。

端口号与IP地址的组合得出一个网络套接字。

端口号被规定为一个 16 位的整数 065535。其中,01023被预先定义的服务通信占用(如telnet占用 端口23,http占用端口80等)。除非我们需要访问这些特定服务,否则,就应该使用 1024~65535 这些 端口中的某一个进行通信,以免发生端口冲突。

利用套接字(Socket)接口开发网络应用程序早已被广泛的采用,以至于成为事实上的标准。套接字能执 行7种基本操作:

• 连接到远程主机

• 绑定到端口

• 接收从远程机器来的连接请求

• 监听到达的数据

• 发送数据

• 接收数据

• 关闭连接

  1. Socket简介

两个Java应用程序可通过一个双向的网络通信连接实现数据交换,这个双向链路的一段称为一个Socket (套接字)。Socket通常用来实现Client/Server 连接。

Java语言的基于套接字编程分为服务器编程和客户端编程,其通信模型如图所示:

一个简单的案例:服务器端与客户端之间是通过流进行交互的。请回顾,输入流与输出流

该案例实现如下效果,客户端与服务器端进行交流,如果客户端输入的是hello,服务器端返回world. 传输普通 的文字

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ClientTest {
public static void main(String[] args) {
System.out.println("客户端启动");
try {
Socket socket = new Socket("localhost",8888);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("你好,服务器".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ServerTest {
public static void main(String[] args) {
try {
System.out.println("客户端已经启动");
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = serverSocket.accept();
InputStream is = socket.getInputStream();
byte[]bs = new byte[1024];
is.read(bs);
String s = new String(bs);
System.out.println("接收到的信息:"+s.trim());
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Test1_Server {
public static void main(String[] args) throws IOException {
System.out.println("服务开启");
ServerSocket server = new ServerSocket(12456);
Socket socket = server.accept();
InputStream inStream = socket.getInputStream();
int i = -1;
byte[] bs = new byte[1024];
inStream.read(bs);
String s = new String(bs);
OutputStream os = null;
System.out.println(s);
if(null!=s&&s.trim().equals("hello")) {
os = socket.getOutputStream();
os.write("world".getBytes());
}
inStream.close();
if(null!=os) {
os.close();
}
socket.close();
server.close();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test1_Client {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.println("请输入要传递的内容");
String words = input.next();
try {
Socket socket = new Socket("localhost", 12456);
OutputStream os = socket.getOutputStream();
os.write(words.getBytes());
InputStream inStream = socket.getInputStream();
byte[] bs=new byte[1024];
inStream.read(bs);
String s = new String(bs);
System.out.println(s.trim());
os.close();
inStream.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

传输普通的图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ClientTest01 {
public static void main(String[] args) {
try {
Socket socket = new Socket("localhost",8888);
OutputStream out = socket.getOutputStream();
FileInputStream inputStream = new FileInputStream("D:/info4.png");
int i;
while((i=inputStream.read())!=-1){
out.write(i);
}
out.close();
inputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ServerTest01{
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
FileOutputStream out = new FileOutputStream("bak.png");
int i ;
while((i=inputStream.read())!=-1){
out.write(i);
}
out.close();
inputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.openlab.net;
import com.openlab.test.OtherClass;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class ClientTest01 {
//客户端给服务器端发送图片,并收到服务器端的返回的信息
public static void main(String[] args) {
try {
Socket socket = new Socket("localhost",8888);
OutputStream out = socket.getOutputStream();//output
FileInputStream inputStream = new FileInputStream("D:/info4.png");
int i;
while((i=inputStream.read())!=-1){
out.write(i);
}
socket.shutdownOutput();//关闭output
InputStream inputStream1 = socket.getInputStream();
byte[] bs = new byte[1024];
inputStream1.read(bs) ;
String s = new String(bs);
System.out.println("收到服务器端信息:"+s.trim());
out.close();
inputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ServerTest01{
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
FileOutputStream out = new FileOutputStream("bak.png");
int i ;
while((i=inputStream.read())!=-1){
out.write(i);
}
OutputStream outputStream = socket.getOutputStream();
outputStream.write("已经收到信息".getBytes());
outputStream.close();
inputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

UDP:

和TCP编程相比,UDP编程就简单得多,因为UDP没有创建连接,数据包也是一次收发一个,所以没有 流的概念。在Java中使用UDP编程,仍然需要使用Socket,因为应用程序在使用UDP时必须指定网络接 口(IP)和端口号。注意:UDP端口和TCP端口虽然都使用0~65535,但他们是两套独立的端口,即一 个应用程序用TCP占用了端口1234,不影响另一个应用程序用UDP占用端口1234。在服务器端,使用 UDP也需要监听指定的端口。Java提供了 DatagramSocket 来实现这个功能。UDP数据报通过数据报套 接字DatagramSocket发送和接收,系统不保证UDP数据报一定能够安全送达目的地,也不能确定什么 时候可以抵达。DatagramPacket对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号 以及接收端的IP地址和端口号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Send{
public static void main(String[] args) {
try {
DatagramSocket datagramSocket = new DatagramSocket();
String s = "我的UDP方式发送的信息";
byte[] bs = s.getBytes();
InetAddress inetAddress = InetAddress.getLocalHost();
DatagramPacket packet = new DatagramPacket(bs,0,bs.length,inetAddress,8888);
datagramSocket.send(packet);
datagramSocket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Receive{
public static void main(String[] args) {
try {
DatagramSocket datagramSocket = new DatagramSocket(8888);
byte[] bs = new byte[1024];
DatagramPacket packet = new DatagramPacket(bs,0,bs.length);
datagramSocket.receive(packet);
System.out.println(new String(packet.getData(),0,packet.getLength()));
} catch (Exception e) {
e.printStackTrace();
}
}
}

URL编程:URL(Uniform Resource Locator)统一资源定位符,它标识Internet上某一资源的地址。 它是一种具体的URL,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
HttpURLConnection urlConnection = null;
InputStream inputStream = null;
FileOutputStream out = null;
try {
URL url = new URL("http://localhost:8080/jspdemo01_war_exploded/test.bmp");
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.connect();
inputStream = urlConnection.getInputStream();
int i ;
out = new FileOutputStream("bak1.png");
while((i=inputStream.read())!=-1){
out.write(i);
}
out.close();
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}

HTTP:

什么是HTTP?HTTP就是目前使用最广泛的Web应用程序使用的基础协议,例如,浏览器访问网站,手 机App访问后台服务器,都是通过HTTP协议实现的。HTTP是HyperText Transfer Protocol的缩写,翻 译为超文本传输协议,它是基于TCP协议之上的一种请求-响应协议。

我们来看一下浏览器请求访问某个网站时发送的HTTP请求-响应。当浏览器希望访问某个网站时,浏览 器和网站服务器之间首先建立TCP连接,且服务器总是使用 80 端口和加密端口 443 ,然后,浏览器向 服务器发送一个HTTP请求,服务器收到后,返回一个HTTP响应,并且在响应中包含了HTML的网页内 容,这样,浏览器解析HTML后就可以给用户显示网页了。一个完整的HTTP请求-响应如下。

image-20220323145401697

HTTP请求的格式是固定的,它由HTTP Header和HTTP Body两部分构成。第一行总是 请求方法 路径 HTTP版本 ,例如, GET / HTTP/1.1 表示使用 GET 请求,路径是 / ,版本是 HTTP/1.1 。

后续的每一行都是固定的 Header: Value 格式,我们称为HTTP Header,服务器依靠某些特定的 Header来识别客户端请求,例如:

Host:表示请求的域名,因为一台服务器上可能有多个网站,因此有必要依靠Host来识别用于请求;

User-Agent:表示客户端自身标识信息,不同的浏览器有不同的标识,服务器依靠User-Agent判断客户 端类型;

Accept:表示客户端能处理的HTTP响应格式, / 表示任意格式, text/* 表示任意文本, image/png 表 示PNG格式的图片;

Accept-Language:表示客户端接收的语言,多种语言按优先级排序,服务器依靠该字段给用户返回特 定语言的网页版本。

如果是 GET 请求,那么该HTTP请求只有HTTP Header,没有HTTP Body。如果是 POST 请求,那么该 HTTP请求带有Body,以一个空行分隔。一个典型的带Body的HTTP请求如下。

1
2
3
4
5
POST /login HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 30
username=hello&password=123456

POST 请求通常要设置 Content-Type 表示Body的类型, Content-Length 表示Body的长度,这样服务 器就可以根据请求的Header和Body做出正确的响应。

此外, GET 请求的参数必须附加在URL上,并以URLEncode方式编码,例如:http://www.example.co m/?a=1&b=K%26R ,参数分别是 a=1 和 b=K&R 。因为URL的长度限制,GET 请求的参数不能太多, 而 POST 请求的参数就没有长度限制,因为 POST 请求的参数必须放到Body中。并且, POST 请求的参 数不一定是URL编码,可以按任意格式编码,只需要在 Content-Type中正确设置即可。常见的发送 JSON的 POST 请求如下:

1
2
3
4
POST /login HTTP/1.1
Content-Type: application/json
Content-Length: 38
{"username":"bob","password":"123456"}

HTTP响应也是由Header和Body两部分组成,一个典型的HTTP响应如下:

1
2
3
4
5
6
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 133251
<!DOCTYPE html>
<html><body>
<h1>Hello</h1>

响应的第一行总是 HTTP版本 响应代码 响应说明 ,例如, HTTP/1.1 200 OK 表示版本是HTTP/1.1 ,响 应代码是 200 ,响应说明是 OK 。客户端只依赖响应代码判断HTTP响应是否成功。

HTTP有固定的响应代码:

1xx:表示一个提示性响应,例如101表示将切换协议,常见于WebSocket连接;

2xx:表示一个成功的响应,例如200表示成功,206表示只发送了部分内容;

3xx:表示一个重定向的响应,例如301表示永久重定向,303表示客户端应该按指定路径重新发送请 求;

4xx:表示一个因为客户端问题导致的错误响应,例如400表示因为Content-Type等各种原因导致的无效 请求,404表示指定的路径不存在;

5xx:表示一个因为服务器问题导致的错误响应,例如500表示服务器内部故障,503表示服务器暂时无 法响应。

当浏览器收到第一个HTTP响应后,它解析HTML后,又会发送一系列HTTP请求,例如, GET /logo.jpg HTTP/1.1 请求一个图片,服务器响应图片请求后,会直接把二进制内容的图片发送给浏览器:

1
2
3
4
HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Length: 18391
????JFIFHH??XExifMM?i&??X?...(二进制的JPEG图片)

因此,服务器总是被动地接收客户端的一个HTTP请求,然后响应它。客户端则根据需要发送若干个 HTTP请求。

对于最早期的HTTP/1.0协议,每次发送一个HTTP请求,客户端都需要先创建一个新的TCP连接,然后, 收到服务器响应后,关闭这个TCP连接。由于建立TCP连接就比较耗时,因此,为了提高效率, HTTP/1.1协议允许在一个TCP连接中反复发送-响应,这样就能大大提高效率

image-20220323145619489

因为HTTP协议是一个请求-响应协议,客户端在发送了一个HTTP请求后,必须等待服务器响应后,才能 发送下一个请求,这样一来,如果某个响应太慢,它就会堵住后面的请求。

所以,为了进一步提速,HTTP/2.0允许客户端在没有收到响应的时候,发送多个HTTP请求,服务器返 回响应的时候,不一定按顺序返回,只要双方能识别出哪个响应对应哪个请求,就可以做到并行发送和 接收:

image-20220323145640571

可见,HTTP/2.0进一步提高了效率。

HTTP编程

既然HTTP涉及到客户端和服务器端,和TCP类似,我们也需要针对客户端编程和针对服务器端编程。

本节我们不讨论服务器端的HTTP编程,因为服务器端的HTTP编程本质上就是编写Web服务器,这是一 个非常复杂的体系,也是JavaEE开发的核心内容,我们在后面的章节再仔细研究。

本节我们只讨论作为客户端的HTTP编程。

因为浏览器也是一种HTTP客户端,所以,客户端的HTTP编程,它的行为本质上和浏览器是一样的,即 发送一个HTTP请求,接收服务器响应后,获得响应内容。只不过浏览器进一步把响应内容解析后渲染并 展示给了用户,而我们使用Java进行HTTP客户端编程仅限于获得响应内容。

我们来看一下Java如果使用HTTP客户端编程。Java标准库提供了基于HTTP的包,但是要注意,早期的 JDK版本是通过 HttpURLConnection 访问

HTTP,典型代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
HttpURLConnection urlConnection = null;
InputStream inputStream = null;
FileOutputStream out = null;
try {
URL url = new URL("http://localhost:8080/jspdemo01_war_exploded/test.bmp");
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.connect();
inputStream = urlConnection.getInputStream();
int i ;
out = new FileOutputStream("bak1.png");
while((i=inputStream.read())!=-1){
out.write(i);
}
out.close();
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}

二十、JDK8新特性

1、日期类型

Java 8 吸收了 Joda-Time 的精华,以一个新的开始为 Java 创建优秀的 API。新的 java.time 中包含了所 有关于本地日期(LocalDate)、本地时间(LocalTime)、本地日期时间(LocalDateTime)、时区 (ZonedDateTime)和持续时间(Duration)的类。历史悠久的 Date 类新增了 toInstant() 方法,用 于把 Date 转换成新的表示形式。这些新增的本地化时间日期 API 大大简化了日期时间和本地化的管理

LocalDate、LocalTime、LocalDateTime 类是其中较重要的几个类,它们的实例是不可变的对象,分别 表示使用 ISO-8601日历系统的日期、时间、日期和时间。它们提供了简单的本地日期或时间,并不包含 当前的时间信息,也不包含与时区相关的信息。

LocalDate代表IOS格式(yyyy-MM-dd)的日期,可以存储 生日、纪念日等日期。

LocalTime表示一个时间,而不是日期。

LocalDateTime是用来表示日期和时间的,这是一个最常用的类之一。

注:ISO-8601日历系统是国际标准化组织制定的现代公民的日期和时间的表示法,也就是公历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	public static void test01() {
LocalDate time = LocalDate.now();// 获取系统当前的时间
System.out.println(time);
time = LocalDate.of(2018, 3, 25);//将整数转为为日期类型
time = LocalDate.of(2018, Month.JUNE, 25);//将整数转换为日期类型
System.out.println(time);
time = LocalDate.parse("2018年04月08号",
DateTimeFormatter.ofPattern("yyyy年MM月dd号"));//将一个字符串转换为日期类型
System.out.println(time.isLeapYear());//是否是闰年
System.out.println(time);
Period p = time.until(LocalDate.now());//获取两个日期的时间差,得到的是Period类型

System.out.println(p.getYears()+"年"+p.getMonths()+"月"+p.getDays()+"天");//可以通过period类型获取 年月日

long t = ChronoUnit.DAYS.between(time, LocalDate.now());//求两个日期差了多少天
System.out.println(t);
DayOfWeek d = time.getDayOfWeek();//获取该日期是星期几
System.out.println(d.getValue());//获取星期几的数字表示
}
1
2
3
4
5
6
7
public static void test02() {
LocalTime time = LocalTime.now();//系统当前的时间,时分秒毫秒
String s = time.format(DateTimeFormatter.ofPattern("HH:mm:ss"));//转换
System.out.println(time);
System.out.println(s);
}

1
2
3
4
5
6
7
8
9
10
public static void test03() {
LocalDateTime time = LocalDateTime.now();
System.out.println(time);
int year = time.getYear();
int month = time.getMonth().getValue();
int day = time.getDayOfMonth();
int week = time.getDayOfWeek().getValue();
System.out.println(year+"年"+month+"月"+day+"日\t"+week);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Instant instant = Instant.now();
System.out.println(instant);

//添加时间的偏移量
OffsetDateTime offsetDateTime =
instant.atOffset(ZoneOffset.ofHours(8));
System.out.println(offsetDateTime);

//toEpochMilli():获取自1970年1月1日0时0分0秒(UTC)开始的毫秒数 ---> Date类的getTime()
long milli = instant.toEpochMilli();
System.out.println(milli);
//ofEpochMilli():通过给定的毫秒数,获取Instant实例 -->Date(long millis)
Instant instant1 = Instant.ofEpochMilli(1550475314878L);
System.out.println(instant1);

2、Lambda表达式

lambda 表达式,也可称为[闭包],它是推动Java 8 发布的最重要新特性,允许把[函数]作为一个方法的 参数(函数作为参数传递进方法中),Java中的Lambda可以被当做是匿名内部类的“语法糖”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void test01(){
Runnable run = new Runnable() {
@Override
public void run() {
System.out.println("run接口中的run方法");
}
};
run.run();
System.out.println("-----------------------------------");
Runnable run1 = ()->{
System.out.println("run接口中的run方法");
};
run1.run();
}
public static void test02(){
Comparator<Integer> com = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
};
com.compare(20,35);
System.out.println("--------------------");
Comparator<Integer> com2 =(o1,o2)-> {return o1.compareTo(o2);};
//方法引用
Comparator<Integer> com3=Integer::compareTo;
}

Lambda表达式的本质:作为函数式接口的实例。

语法:

1
2
3
parameters) -> expression

(parameters) ->{ statements; }

( ) :用来描述参数列表;

{ } : 用来描述方法体;

-> :Lambda运算符,可以叫做箭头符号,或者goes to

3、函数式(Functional)接口:如果一个接口中,只声明了一个抽象方法,则此接口就称为函数式接口。

二一、JSON

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//字符串转换为json对象,以及java对象
public static void test(){
//字符串转换为json对象
String str = "{\"name\":\"zhangsan\",\"sex\":\"male\",\"age\":\"18\"}";
JSONObject obj =JSONObject.parseObject(str);
System.out.println(obj);
String name = obj.get("name").toString();
System.out.println(name);
String name2 = obj.getString("name");
System.out.println(name2);
int age = obj.getInteger("age");
System.out.println(age);
//json字符串转换为普通java对象
Person p = (Person) JSONObject.parseObject(str,Person.class);
System.out.println(p.getSex());
}

1
2
3
4
5
6
7
8
9
10
11
12
13
//java对象转换为json字符串以及json对象
public static void test2(){
Person p = new Person();
p.setName("李四");
p.setAge(18);
p.setSex("男");
//java对象转换为json字符串
String s = JSONObject.toJSONString(p);
System.out.println(s);
//java对象转换为json对象
JSONObject json= (JSONObject) JSONObject.toJSON(p);
System.out.println(json.getString("name"));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//普通java集合转换为json数组
public static void test3(){
List<Person> list = new ArrayList<>();
Person p1 = new Person("A","男" ,17);
Person p2 = new Person("B","女" ,16);
Person p3 = new Person("C","男" ,17);
list.add(p1);
list.add(p2);
list.add(p3);
JSONArray arr = (JSONArray) JSONArray.toJSON(list);
for(int i=0;i<arr.size();i++){
JSONObject json = (JSONObject) arr.get(i);
System.out.println(json.get("name"));
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void test4(){
String s="[{\"name\":\"zhangsan\",\"sex\":\"male\",\"age\":\"18\"},{\"name\":\"zhangsan1\",\"sex\":\"male1\",\"age\":\"17\"}]";
List<Person> list = JSONObject.parseArray(s,Person.class);
for (int i=0;i<list.size();i++){
Person p = list.get(i);
System.out.println(p.getName()+"\t"+p.getSex());
}
System.out.println("-------------------------------");
JSONArray arr = JSONObject.parseArray(s);
for(int i=0;i<arr.size();i++){
JSONObject json = (JSONObject) arr.get(i);
System.out.println(json.get("name"));
}
}