Java 入门整理的笔记
工具
JDK
JDK是Java开发工具包,其中包含javac
与java
两个程序,使用Java编程需要JDK。
Java编译运行
Java程序的编译运行是跨平台(指操作系统)的,主要归功于JVM,在Java程序的编译运行分为三步(编写、编译、运行。可直接使用命令行操作):
- 编写源代码
.java
文件(源代码)通过javac.exe
程序(编译器)编译成.class文件.class
文件(又称字节码文件)通过java.exe
(解释器)转换成机器码,原理是放入对应平台的Java虚拟机(JVM)内转化为计算机可运行二进制机器码
JRE
JRE是Java运行环境,其中包含了JVM,由上步骤可知,想要运行Java程序,必须要有JRE。在安装JDK时,JDK中已经自带了JRE,所以不需要单独安装JRE
为什么要配置环境变量
当你在命令行中使用 javac
或 java
时,需要先进入到jdk的安装目录中的 bin
文件目录下,才能运行Java命令,但是在命令行中运行记事本时,你只并不需要找到记事本的安装目录,直接输入notepad命令即可,这是因为计算机知道去哪里找记事本的目录,但是不知道去哪找Java的目录,为了能在命令行窗口中,直接运行Java,去掉繁琐的输目录的过程,所以我们需要在环境变量中添加路径。(即配置环境变量)
- 首先在系统变量栏添加一个
JAVA_HOME
来代替bin
目录之前的一长串目录。 - 然后再在系统变量栏里找到
Path
,编辑一条%JAVA_HOME%\bin
,%JAVA_HOME%
就是代替了bin
之前的一长串目录而已。 - 这样我们就把Java的路径添加到了环境变量中,计算机在收到Java指令的时候就知道去Java的目录下去寻找Java程序了。
基础概念
关键字
完全小写的字母。 在增强版的记事本(notepad++)或者其他IDE当中有特殊颜色。
标识符
类、方法、变量的名称,自己取的那种,但是
要求: - 英文字母(区分大小写)、数字、$
(不推荐,因为后面学到的内部类的class文件命名方式包含了此符号,见 23 内部类)和_
- 不能以数字开头 - 不能是关键字
建议: - 类名:大驼峰式 - 变量名:小驼峰式 - 方法名:小驼峰式
常量
程序运行期间固定不变的量
常量的分类: 1. 字符串常量:凡是用双引号引起来的部分,叫做字符串常量。 2. 整数常量:直接写上的数字,没有小数点。 3. 浮点数常量:直接写上的数字,有小数点。 4. 字符常量:凡是用单引号引起来的单个字符。叫做字符常量。(Java中 一个中文也是一个字符)两个单引号中间必须有且仅有一个字符,没有不行;有两个也不行。 5. 布尔常量:true、false 6. 空常量:null 代表没有任何数据 不能直接用来打印输出
变量
与常量相反。程序运行期间,内容可以发生改变的量。注意:变量赋值时,右侧数值范围不能超过左侧的数据范围 ,long与float赋值带后缀L与F
注意事项: 1. 如果创建多个变量,那么变量之间的名称不可以重复 2. 对于 float
和 long
类型来说,字母后缀F和L不要丢掉 3. 如果使用 byte
或者 short
类型的变量,右侧的数据值,不能超过左侧类型的范围 4. 没有进行赋值的变量不能直接使用,一定要赋值之后才能使用 5. 变量的使用不能超过作用域的范围。作用域: 从定义变量的一行开始,一直到直接所属的大括号结束为止 6. 可以通过一个语句来创建多个变量,但是一般情况下不推荐。
数据类型
基本数据类型
四类八种: - 整数型 byte short int long
- 浮点型 float double
但是浮点数往往会有精度损失。所以在定义货币的时候,没有1.11元的说法,往往都是111分。详情请看《计算机组成原理》中存储浮点数的底层原理 - 字符型 char
- 布尔型 boolean
数据类型 | 关键字 | 内存占用 | 取值范围 |
---|---|---|---|
字节型 | byte | 1个字节(8位) | -128~127 ($ -2727-1 $) |
短整型 | short | 2个字节 | -32768~32767 |
整型 | int(默认) | 4个字节 | $ -2{31}2{31}-1$(大约21个亿) |
长整型 | long | 8个字节 | $ -2{63}2{63}-1$ |
单精度浮点数 | float | 4个字节 | 1.4013E-45~3.4028E+38 |
双精度浮点数 | double(默认) | 8个字节 | 4.9E-324~1.7977E+308 |
字符型 | char | 2个字节 | 0-65535 |
布尔类型 | boolean | 1个字节 | true false |
注意事项: 1. 字符串不是基本类型,而是引用类型 2. 浮点型可能只是一个近似值,并非精确的值 3. 数据范围与字节数不一定相关, float
(4字节)数据范围比 long
(8字节)更加广泛。 4. 浮点数当中默认类型是 double
。如果一定要用 float
,需要加上一个后缀 F。如果是整数,默认为 int
类型,如果一定要用 long
,需要加上一个后缀 L。F L 大小写都可以,推荐大写
引用数据类型
不是基本数据类型 就是 引用数据类型 只有这两种。比如后面要介绍的字符串、数组、类、接口、Lambda等
数据类型转换
当数据类型不一样时,将会发生数据类型转换
一句话概括: 对于一行赋值语句某个变量 = 某个变量/常量
首先如果右边是常量,判断常量的值是否超过左边的类型对应的数值范围,若超过了,直接报错! 然后判断左右两边类型的数值范围大小,若由小到大(右边小,左边大),可自动转。否则,需要强转!
- 自动类型转换(隐式)
- 特点:代码不需要进行特殊处理,自动完成
- 规则:数据范围从小到大,与字节数不一定相关 数据范围规则: byte、short、char --> int --> long --> float --> double
- 强制类型转换(显式)
- 特点:代码需要进行特殊的格式处理,不能自动完成。
- 格式:
范围小的类型 范围小的变量名 = (范围小的类型)原本范围大的数据
注意事项: 1. 强制类型转换一般不推荐使用,因为有可能发生精度损失、数据溢出。 2. byte/short/char
这三种类型都可以发生数学运算。 3. byte/short/char
在运算的时候,都会被首先提升为int类型,然后再计算。4.2和4.6的代码块都有用到 4. boolean
类型不能发生任何数据类型转换
1 | public class Demo01DataType{ |
1 | public class Demo02DataType { |
编码表
数字和字符的对照关系表(编码表): - ASCII: American Standard Code for Information Interchange
美国信息交换标准代码,48代表'0';65代表大写字母'A';97代表小写字母'a' ;一个中文占2个字节 - Unicode
万国码,开头0-127部分和ASCII完全一样,但是从128开始包含有更多字符。其中 UTF-8
:一个中文占3个字节
运算符
运算符:进行特定操作的符号 表达式:用运算符连起来的式子
算数运算符
- 四则运算符:加减乘除(`+ - * /
) 对于整数表达式来说,除法
/`用的是整除,只看商,不看余数 - 取模运算(取余数):
%
- 自增运算符:
++
;自减运算符:--
- 单独使用时,
前++
与后++
没有任何区别 - 混合使用时:
- 如果是
前++
,立刻让变量+1,然后拿着结果进行使用 - 如果是
后++
,首先使用变量本来的数值,然后再让变量+1
- 如果是
- 单独使用时,
注意事项: 1.一旦运算当中有不同类型的数据,那么结果将会是数据类型范围大的那种 2.只有变量才能使用自增、自减运算符。常量不能改变,所以不能用
1 | public class Demo06operator { |
赋值运算符
分为:基本赋值运算符=
和复合赋值运算符+=
-=
*=
/=
%=
注意事项: 1. 只有变量才能使用赋值运算符,常量不能进行赋值 2. 复合赋值运算符其中隐含了一个强制类型转换
1 | byte num = 30; |
比较运算符
==
<
>
<=
>=
!=
变量与变量之间,常量与常量之间,变量与常量之间,都可以使用。
注意事项: 1. 比较运算符的结果一定是一个boolean值,成立就是true,不成立就是false。 2. 如果进行多次判断,不能连着写。 3. 关于==
详情见 Object 类中的 equals
方法
逻辑运算符
短路与(并且) &&
短路或(或者) ||
非(取反) !
&&
与||
具有短路效果,节省性能。
注意事项: 1. 逻辑运算符只能用于boolean值 2. 与、或需要左右各有一个boolean。取反只需要一个 3. 与、或两种运算符,如果有多个条件,可以连续写
三元运算符
格式: 数据类型 变量名称 = 条件判断 ? 表达式A : 表达式B;
首先判断条件是否成立: - 如果成立为true,将表达式A的值赋值给左侧的变量 - 如果不成立为false,将表达式B的值赋值给左侧的变量 二者选其一
注意事项: 1. 必须同时保证表达式A和表达式B都符合左侧的数据类型的要求。 2. 三元运算符的结果必须被使用,要么赋值,要么直接输出。
编译器优化
- 对于
byte short char
类型赋值,如果右侧赋值数值没有超过范围,编译器自动补上强转;若超过范围,编译器报错。 - 在给变量进行赋值的时候,如果右侧的表达式当中全都是常量,没有任何变量。那么编译器会直接将若干个常量表达式计算得到结果。
1 | short a = 5; // 5 没有超过 short的范围,正确 a是short型 |
上述记录于2019.10.14
流程控制
需要清楚每条语句的执行流程,即各条语句的执行顺序
顺序结构
这个就不解释了。。。。
判断结构
- if语句第一种格式:
if
1 | if(关系表达式) { |
- if语句第二种格式:
if...else
1 | if(关系表达式) { |
- if语句第三种格式:
if...else if...else
1 | if (判断条件1) { |
选择结构
选择语句: switch
1 | switch(表达式) { // 括号里就是一个表达式,计算结果不一定是一个boolean值 |
1 | /* |
循环结构
循环结构的基本组成部分,一般可以分成四部分: 1. 初始化语句:在循环开始最初执行,而且只做唯一一次。 2. 条件判断: 如果成立,则循环继续;如果不成立,则循环退出。 3. 循环体:重复要做的事情内容,若干行语句。 4. 步进语句:每次循环之后都要进行的扫尾工作,每次循环结束之后都要执行一次。
- 循环语句1:
for
1 | for (初始表达式;布尔表达式;步进表达式) { |
- 循环语句2:
while
1 | 初始表达式; |
- 循环语句3:
do-while
1 | 初始化表达式; |
三种循环的区别: - 如果条件判断从来没有满足过,那么for
循环和wile
循环将会执行0次,但是do-while
循环会执行至少一次 - for
循环的变量在小括号当中定义,只有循环内部才可以使用。while
循环和do-while
循环初始化语句本来就在外面,所以出来循环之后还可以继续使用。
小建议:凡是次数确定的场景,多用for循环;否则多用while循环
- 死循环 永远停不下来的循环,编译可以通过,可以运行,有时会使用到
1 | while (true) { |
但是死循环后面加上的语句无法执行,会报错!
1 | public class Demo16DeadLoop { |
- 循环的嵌套 很好理解,循环内部写一个循环。
条件控制
break
语句
break的用法有常见的两种: 1. 可以用在switch语句到那个中,一旦执行,整个switch语句立刻结束 2. 还可以用在循环语句当中,一旦执行,整个循环语句立刻结束。打断循环。
continue
语句
一旦执行,立刻跳过当前次循环剩余内容,马上开始下一次循环。
1 | public class Demo15Continue { |
上述记录于2019.10.15
# IDE
方便java开发的软件。 存储结构:一个项目project
---(多个)模块module
----(多个)包package
---- 文件(可以是包,可以是java文件)
常用的Intelij IDEA 快捷键 : alt + 空格
导入包 ctrl + Y
删除当前行 ctrl + D
复制当前行 ctrl + alt + L
格式化 ctrl + /
添加注释 ctrl + shift + /
多行注释 alt + ins
自动生成代码 alt + shift + 上下箭头
移动代码行 alt + 回车
自动添加代码!很常用!我经常用它来 Introduce local variable
# 方法
将一个功能抽取出来,把代码单独定义在一个大括号内,形成一个单独的功能 当我们需要这个功能时,就可以去调用。这样即实现了代码的复用性,也解决了代码冗余的现象。
方法的概念
方法其实就是若干语句的功能集合
方法好比一个工厂。 蒙牛工厂 原料:奶牛、饲料、水 产出物:奶制品 钢铁工厂 原料:铁矿石、煤炭 产出物:钢铁建材 参数(原料):进入方法的数据 返回值(产出物):从方法中出来的数据
定义方法
格式:
1 | 修饰符 返回值类型 方法名称(参数类型 参数名称, ...) { |
修饰符: 现阶段的固定写法,public static 返回值类型: 也就是方法最终产生的数据结果是什么类型 方法名称: 方法的名字,规则和变量一样. 小驼峰 参数类型:进入方法的数据是什么类型 参数名称:进入方法的数据对应的变量名称 PS: 参数如果有多个,使用逗号进行分隔 方法体:方法需要做的事情,若干行代码 return: 两个作用。1. 停止当前方法 2.将后面的结果数据(返回值)还给调用处 返回值:也就是方法执行后最终产生的数据结果 return 后面的返回值,必须和方法名称前面的返回值类型,保持对应
方法的调用格式
- 单独调用: 方法名称(参数);
- 打印调用: System.out.println(方法名称(参数));
- 赋值调用: 数据类型 变量名称 = 方法名称(参数);
注意:无返回值的方法,只能进行单独调用!
方法的调用流程
- 找到方法
- 参数传递
- 执行方法体
- 若有返回值,带着返回值回到方法的调用处
方法定义的注意事项
- 方法应该定义在类当中,不能在方法中再定义方法。不能嵌套
- 方法定义的前后顺序无所谓
- 方法定义之后不会执行,如果希望执行,一定要调用:单独调用、打印调用、赋值调用
- 如果方法有返回值,必须写“return 返回值;” 不能没有
- return后面的返回值数据,必须和方法的返回值类型 对应起来
- 对于一个void没有返回值的方法,不能写return后面的返回值, 只能写return自己。
- 对于void方法当中最后一行的return可以省略不写
- 一个方法当中可以有多个return语句,但是必须保证同时只有一个会被执行到。
- 两个return不能连写
Overload 重载
本来可以归并到方法里讲的,但是我给他单独列出来,因为它的确很重要,而且很容易和后面的方法覆盖重写 Override 搞混淆。
为什么使用方法重载: 对于功能类似的方法来说,因为参数列表不一样,却要记住那么多不同的方法名称,太麻烦 可不可以只需要记住一个方法名称,就可以实现类似的多个功能呢?
方法的重载(Overload): 多个方法的名称一样,但是参数列表不一样。
方法重载与下列因素相关:
- 参数个数不同 (可变参数在参数类型确定、个数不确定时使用)
- 参数类型不同
- 参数的多类型顺序不同
- 方法重载与下列因素无关
- 参数名称
- 方法的返回值类型
1 | public static void open() {...} // 原始方法 |
上述记录于2019.10.16
数组
是一种容器,可以同时存放多个数据值。
- 数组的特点:
- 数组是一种引用类型 (所以直接打印的话出现的是数组的地址值)
- 数组中的多个数据类型必须统一
- 数组的长度在程序运行期间不可改变 (2、3也是和集合的区别所在)
- 数组的初始化: 在内存中创建一个数组,并且向其中赋予一些默认值
- 动态初始化:(指定长度)
- 静态初始化:(指定内容)
- 动态初始化格式
1 | 数据类型[] 数组名称 = new 数据类型[数组长度] |
如果是整数类型,默认为0 如果是浮点类型,默认为0.0 如果是字符类型,默认为'\u0000'
一种特殊的字符,不是空但是打印不出来 如果是布尔类型,默认为false 如果是引用类型,默认为null 可以打印出来
- 静态初始化格式
1 | 数据类型[] 数组名称 = new 数据类型[] {元素1,元素2,...}; |
虽然没指定长度,但是可以根据大括号里的元素自动推算出长度。 也有默认值,只不过系统自动马上将默认值替换为了大括号当中的具体数值。
使用建议: 如果不确定数组当中的具体内容,用动态初始化;否则用静态初始化。
常见问题:
- 如果访问数组元素的时候,索引编号并不存在,将会发生数组索引越界异常:
ArrayIndexOutOfBoundsException
- 数组必须进行new初始化才能使用其中的元素。如果只是赋值了一个null,没有进行new,将会放生空指针异常:
NullPointerException
- 将数组当作方法的参数时,方法接收的是数组的地址。
- 当方法返回参数是数组时,方法返回的是数组的地址。(3、4两点会在11中详细解释!)
- 缺点: 数组一旦创建,程序运行期间,长度不可改变
Java的内存划分
5个部分 1. 栈(Stack): 存放的都是方法中的局部变量。方法的运行一定要在栈当中运行 局部变量:方法的参数,或者是方法{}内部的变量 作用域:一旦超出作用域,立刻从占内存种消失
堆(Heap): 凡是new出来的东西,都在堆当中 成员变量:类中的定义的变量,在方法外边 字符串常量池 详情见14.1 字符串常量池 堆内存里面的东西都有一个地址:16进制 堆内存里面的数据都有默认值:规则同数组。
方法区(Method Area): 存储 .class相关信息,包含方法的信息、常量、静态变量(static)
本地方法栈(Native Method Stack): 与操作系统相关
寄存器(pc Register): 与CPU相关
上述记录于2019.10.19
面向对象
面向过程: 当需要实现一个功能的时候,每一个具体的步骤都要亲力亲为,详细处理每一个细节。
面向对象: 当需要实现一个功能的时候,不关心具体步骤,而是找一个已经具有该功能的人,来帮我做事儿。 面向对象思想是一种更符合我们思考习惯的思想,它可以将复杂的事情简单化,并将我们从执行者变成了指挥者。
特点: 三大基本特征:封装、继承和多态。
类和对象
- 类: 是一组相关属性和行为的集合。可以看成是一类事物的模板
- 属性:是什么
- 行为:能做什么
- 对象:是一类事物的具体体现。
类和对象的关系
- 类是对一类事物的描述,是抽象的
- 对象是一类事物的实例,是具体的
- 类是对象的模板,对象是类的实体
类的定义
成员变量(属性) 成员方法(行为)
注意事项: 1. 成员变量是直接定义在类当中的,在方法外边 2. 成员方法不要写 static
关键字(静态方法是类的方法)
对象的创建
通常情况下,一个类并不能直接使用,需要根据类创建一个对象,才能使用 1. 导包:也就是指出需要使用的类在什么位置 import 包名称,类名称;
对于和当前类属于同一个包的情况,可以省略导包语句不写 2. 创建格式: 类名称 对象名 = new 类名称();
3. 使用,分两种情况: - 使用成员变量: 对象名.成员变量名
- 使用成员方法: 对象名.成员方法名(参数)
也就是,想用谁,就用对象名点儿谁
注意事项: 如果成员变量没有进行赋值,那么将会有一个默认值、规则和数组一样 当一个对象作为参数,传递到方法当中时,实际上传递进去的是对象的地址值 当使用一个对象类型作为方法的返回值时,返回值其实就是对象的地址值
1 | package cn.perdant.day06.demo03; |
封装
- 方法是一种封装
- 关键字
private
也是一种封装 封装就是将一些细节信息隐藏起来,对于外界不可见。
private关键字
一旦使用 private
进行修饰,那么本类当中仍然可以随意访问,但是超出了本类范围之外就不能再直接访问。 间接访问 private
成员变量,定义一堆 Getter/Setter
方法 必须叫 setXXX
或者 getXXX
命名规则 Getter
不能有参数,返回值类型和成员变量对应。 Setter
不能有返回值,参数类型和成员变量对应。
this关键字
当方法的局部变量和类的成员变量重名的时候,根据“就近原则”,优先使用局部变量。 如果需要访问本类当中的成员变量,需要使用格式: this.成员变量
哪个对象调用的方法,方法里的this就是那个对象
上述记录于2019.10.21
构造方法
构造方法是专门用来创建对象的方法,当我们通过关键字new来创建对象时,其实就是在调用构造方法。
1 | public 类名称(参数类型 参数名称) { |
注意事项: 1. 构造方法的名称必须和所在的类名称完全一样,就连大小写都要一样 2. 构造方法不要写返回值类型,连void都不写 3. 构造方法不能return 一个具体的返回值 4. 如果没有编写任何构造方法,那么编译器将会默认赠送一个构造方法,没有参数、方法体什么事情都不做 5. 一旦编写了至少一个构造方法,那么编译器将不再赠送 6. 构造方法也是可以进行重载的
一个标准的类
- 所有的成员变量都要使用
private
关键字修饰 - 为每一个成员变量编写一对儿
Getter/Setter
方法 - 编写一个无参数的构造方法
- 编写一个全参数的构造方法 标准类被称为
Java Bean
上述记录于2019.10.22
匿名对象
只有右边的对象,没有左边的名字和赋值运算符
1 | new 类名称(); |
注意事项: 1. 匿名对象只能使用唯一的一次,下次再用不得不再创建一个新对象。 2. 如果确定有一个对象只需要使用唯一的一次,就可以用匿名对象。
常用API (一)
Application Programing Interface
应用程序编程接口是JDK中提供给我们直接使用的类 API文档就是程序员的字典 主要看 包路径、构造方法、方法摘要
引用类型的一般使用步骤: 1. 导包 只有java.lang
包下的内容不需要导包,其他的包都需要 import
语句 2. 创建 3. 使用
Scanner
一个可以解析基本类型和字符串的文本扫描器。可以实现键盘输入数据,到程序当中。 new Scanner(System.in)
生成一个Scanner对象,并接收输入 int nextInt()
:返回你所输入的整型数 String next()
:返回你所输入的字符串 详情参见API
上述记录于2019.10.25
Random
Random类用来生成随机数字。 nextInt(无参数)
:获取一个随机的int数字(范围是int所有范围,有正负) nextInt(有参数)
:获取一个随机的int数字(参数代表了范围,左闭右开区间) 详情参见API
1 | Random r = new Random(); |
ArrayList
前面说过,数组的长度不可以发生改变。(详情见10) 但是集合ArrayList<E>
的长度是可以随意变化的。(后面还会介绍更多种集合) <E> 代表泛型:也就是装在集合当中的所有元素,全都是统一的什么类型,只能是引用类型,不能是基本类型。 从JDK 1.7+开始,右侧的尖括号内容可以不写,但是<>本身还是要写。 ArrayList<String> arrayList = new ArrayList<>();
注意事项: 1. 对于ArrayList集合来说,直接打印得到的不是地址值,而是内容(说明它覆盖重写了toString方法) 2. 如果内容是空的,得到的是空的中括号:[] 详情参见API
1 | package cn.perdant.day07.demo04; |
1 | package cn.perdant.day07.demo04; |
上述记录于2019.10.26
String 字符串
java.lang.String
类代表字符串。 程序当中所有的双引号字符串,都是String类的对象。(就算没有new,也照样是) 字符串是常量;它们的值在创建之后不能更改。
字符串的特点: 1. 字符串的内容永远不可变 2. 因为字符串内容不可改变,所以可以共享使用 3. 字符串效果上,相当于是char[]
字符数组,但是底层原理是byte[]字节数组
创建字符串的常见3+1种方式: - 三种构造方法 1. public String();
创建一个空白字符串,不含有任何内容 2. public String(char[] array);
根据字符数组的内容。来创建对应的字符串 3. public String(byte[] array);
根据字节数组的内容来创建对应的字符串 - 一种直接创建 String str = "Hello"; // 右边直接用双引号
没有new,直接写上双引号,就是字符串对象
字符串常量池
从JDk1.7+ 常量池在堆内存中。注意:直接双引号的字符串,去常量池里找,没有则在常量池里生成,new的不去常量池!!直接在常量池外new。
1 | /* |
==
是进行对象的地址值比较,如果确实需要字符串的内容比较,可以使用两种方法:
public boolean equals(Object obj);
- 任何对象都能用Object进行接收
- 具有对称性,a.equals(b) 与 b.equals(a) 效果一样
- 如果比较双方一个常量一个变量,推荐把常量字符串写在前面
1 | String str = null; |
public boolean equalsIgnoreCase(String str);
忽略大小写,进行内容比较 只有英文字母区分大小写其他都不区分大小写
String常用方法
String 当中与转换相关的常用方法: public char[] toCharArray()
将当前字符串拆分成为字符数组作为返回值 public byte[] getBytes()
获得当前字符串底层的字节数组 public String replace(CharSequence oldString, CharSequence newString)
将所有出现的老字符串替换成新的字符串,返回替换之后的结果新字符串 CharSequence
是个接口,意思就是说,可以接收字符串类型
String 当中与获取相关的常用方法: public int length()
获取字符串长度 public String concat(String str)
拼接字符串 public char chaAt(int index)
获取指定索引位置的单个字符 public int indexOf(String str)
查找参数字符串在本字符串当中首次出现的索引位置,如果没有返回-1
字符串的截取方法: public String substring(int index)
public String substring(int begin, int end)
左闭右开 [begin, end)
分隔字符串的方法: public String[] split(String regex)
按照参数的规则,将字符串切分成若干部分 regex参数其实是一个正则表达式
上述记录于2019.10.27
静态(static 关键字)
一旦用了static
关键字 那么这样的内容不在属于对象自己而是属于类的 凡是本类的对象,都共享同一份 无论是成员变量还是成员方法,一旦使用了static,都推荐使用类名称进行调用 如果是用对象名称调用,编译器在编译时会自动转换成类名称调用
注意事项: 1. 静态不能直接访问非静态 原因:内存当中是先有静态内容,后有非静态内容 2. 静态方法当中不能用this 原因:this代表当前对象。但静态与对象没有关系,只和类有关系。 3. 关于静态代码块:
1 | public class 类名称 { |
- 当第一次用到本类时,静态代码块执行唯一的一次
- 静态内容总是优先于非静态,所以静态代码块比构造方法先执行
- 静态代码块的典型用途,用来一次性地对静态成员变量进行赋值
常用工具类
下面介绍的几个工具类,都提供了大量的静态方法, 所以不需要new对象,直接可以通过类名调用
Arrays
一个与数组相关的工具类,提供了大量静态方法。用来实现数组的常见操作。 public static String toString(数组)
将参数数组编程字符串,按照默认格式[元素1,元素2,元素3,...]
public static void sort(数组)
按照默认升序(从小到大) 对数组的元素进行排序 如果是数值,默认升序 从小到大 如果是字符串 ,默认按照字母升序 如果是自定义类 这个自定义类需要有Comparable
或者Cmoparator
接口支持(后面要讲的覆盖重写)
Math
是数学相关的工具类,里面提供了大量的静态方法,完成与数学相关的操作
1 | public static double abs(double num); //绝对值 |
Collections
是集合相关的工具类,里面提供了大量的静态方法,完成与集合相关的操作详情见Collections类
上述记录于2019.10.28
继承
继承主要解决的问题就是:共性抽取
继承关系当中的特点: 1. 子类可以拥有父类的内容 2. 子类还可以拥有自己专有的内容
成员变量重名时:
在父子类的继承关系当中,如果成员变量重名,则创建子类对象时,访问有两种方式: 1. 直接通过子类对象访问成员变量 - 等号左边(创建对象时的赋值等号)是谁,就优先用谁,没有则向上找 2. 间接通过成员方法访问成员变量 - 方法属于谁,就优先用谁,没有则向上找 多态口诀:编译看左边,运行还看左边(后面会讲多态)
局部变量和成员变量重名时:
局部变量、本类的成员变量、父类的成员变量重名时如何区分?
1 | public class Fu { |
成员方法重名时:
在父子类的继承关系当中,创建子类对象,访问成员方法的规则: 看右边创建的对象(new 的)是谁,就优先用谁的成员方法,如果没有则向上找。 (多态口诀: 编译看左边,运行看右边!)
1 | Fu obj = new Zi(); |
注意事项: 1. Java语言是单继承的:一个类的直接父类只能由唯一一个。 2. Java语言可以多级继承。最顶端:java.lang.Object
。 3. 一个子类的直接父类是唯一的,但是一个父类可以拥有很多个子类。 4. 无论是调用成员方法还是成员变量,如果没有都是向上找父类,绝对不会向下找子类。
上述记录于2019.10.29
Override(覆盖)重写
概念:在继承关系当中,方法的名称一样,参数列表也一样,也叫覆盖重写 而反观重载(参见重载):方法名称一样,参数列表不一样
方法的覆盖重写特点:创建的是子类对象,优先用子类方法
注意事项: 1. 必须保证父子类之间的方法名称相同,参数列表也相同。 - 可选的安全检测手段 @override
:写在方法前面,用来检测是不是有效的正确覆盖重写 2. 子类方法的返回值,必须小于等于父类方法的返回值 3. 子类方法的权限,必须大于等于父类方法的权限修饰符 - public > protected > (default) > private
- (default)
不是关键字default, 而是什么都不写,留空!
super关键字
前面已经用到过,super
关键字用来访问父类内容。
super关键字的三种用法: 1. 在子类的成员方法中,访问父类的成员变量 2. 在子类的成员方法中,访问父类的成员方法 3. 在子类的构造方法中,访问父类的构造方法
继承关系中,父子类构造方法的访问特点: 1. 子类构造方法当中有一个默认隐含的 super();
调用,所以一定是先调用父类构造方法,后执行的子类构造方法 2. 子类构造可以通过super
关键字,调用父类重载构造 3. super()
的父类构造调用,必须是子类构造方法的第一个语句,不能一个子类构造调用多次super
构造
this关键字
this关键字用来访问本类内容,用法也有三种: 1. 在本类的成员方法中,访问本类的成员变量 2. 在本类的成员方法中,访问本类的另一个成员方法 3. 在本类的构造方法中,访问本类的另一个构造方法 - this(...)
调用也必须是构造方法的第一个语句,也是唯一一个this
。 此时super();
不会赠送。 - super
和this
两种构造调用,不能同时使用。
抽象
抽象方法
如果父类当中的方法不确定如何进行{}
方法体实现,那么这就应该是一个抽象方法
抽象类
抽象类:抽象方法所在的类,必须是抽象类才行。在 class
之前写上 abstract
即可
如何使用抽象类和抽象方法: 1. 不能直接创建 new
抽象类对象 2. 必须用一个子类来继承抽象父类 3. 子类必须覆盖重写抽象父类当中所有的抽象方法 覆盖重写(实现): 子类去掉抽象方法 abstract
关键字,然后补上方法体大括号 4. 创建子类对象进行使用
注意事项: 1. 抽象类不能创建对象 2. 抽象类可以有构造方法,供子类创建对象时,初始化父类成员使用。 3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定时抽象类 4. 抽象类的子类,必须重写抽象父类的所有抽象方法,否则,子类也是一个抽象类
接口
接口就是多个类的公共规范,是一种引用数据类型,最重要的内容就是其中的抽象方法
格式类似类,关键字 class
换成关键字 interface
,编译生成的字节码文件仍然是 .java --> .class
1 | puvlic interface 接口名称{ |
如果是Java 7 1. 常量 [public] [static] [final] 数据类型 常量名称 = 数据值;
2. 抽象方法 [public] [abstract] 返回值类型 方法名称(参数列表);
如果是Java 8 额外添加 3. 默认方法 [public] default 返回值类型 方法名称(参数列表){方法体}
4. 静态方法 [public] static 返回值类型 方法名称(参数列表){方法体}
如果是Java 9 额外添加 5. 私有方法 普通私有方法: private 返回值类型 方法名称(参数列表){方法体}
静态私有方法: private static 返回值类型 方法名称(参数列表){方法体}
上述的带中括号[]的都是默认可以省略不写的,其中default 和之间在18中提到的(defualt)不同,不是留空,是要真正写出来的!
接口使用的步骤: 1. 接口不能直接使用,必须有一个“实现类” 来 “实现” 该接口 2. 接口的实现类 implements 接口的类
,必须覆盖重写(实现)接口中所有的抽象方法。 3. 创建实现类的对象,进行使用
接口中的抽象方法
- 接口当中的抽象方法,修饰符必须是两个固定的关键字:public abstract,可以选择性的省略,方法的三要素(名称,参数,返回值)可以随意
- 接口的实现类(
implements
接口的类),必须覆盖重写(实现)接口中所有的抽象方法。如果实现类没有覆盖重写接口中所有的抽象方法,那么这个实现类自己就必须是抽象类!
接口中的默认方法
- 接口当中的默认方法,可以解决接口升级的问题(接口在投入使用后,又要在接口中加入新的方法)固定关键字public default 其中public可以省略
- 接口的默认方法,可以通过接口实现类对象直接调用
- 接口的默认方法,也可以被接口的实现类进行覆盖重写
接口中的静态方法
- 不能通过接口实现类的对象来调用接口当中的静态方法,应该通过接口名称直接调用其中的静态方法
上述记录于2019.10.30
接口中的私有方法
- 普通私有方法,解决多个默认方法之间重复代码问题
- 静态私有方法,解决多个静态方法之间重复代码问题
接口中的常量(成员变量)
- 必须使用public static final修饰(可以省略不写)
- 必须赋值(没有默认值),赋值后不可修改
- 常量用完全大写字母,用下划线分隔
注意事项: 1. 接口不能有静态代码块和构造方法 2. 一个类的直接父类是唯一的,但是一个类可以同时实现多个接口 3. 如果实现类所实现的多个接口当中,存在重名的抽象方法,那么只需要覆盖重写一次即可。 4. 如果实现类所实现的多个接口当中,存在重名的默认方法,那么实现类要对重名的默认方法覆盖重写! 5. 如果实现类没有覆盖重写接口中所有的抽象方法,那么这个实现类自己就必须是抽象类! 6. 一个类如果直接父类当中的方法,和接口当中的默认方法产生了冲突(重名),优先用父类的默认方法
继承与实现: 1. 类与类之间是单继承的,直接父类只有一个 2. 类与接口之间是多实现的,一个类可以实现多个接口 3. 接口与接口之间是多继承的 4. 多个父接口中的抽象方法重名,没关系,子类只需要覆盖重写一个即可。 5. 多个父接口中的默认方法重名,子接口必须默认重写默认方法,而且带着 default
!
多态 (左父右子)
extends
继承或者implements
实现,是多态的前提。 代码当中体现多态性,其实就是一句话:父类引用指向子类对象 格式: 父类名称 对象名 = new 子类名称();
或者 接口名称 对象名 = new 实现类名称();
具体规则参见继承章节!
对象的向上转型
其实就是多态写法 父类名称 对象名 = new 子类名称(); Animal animal = new Cat();
右侧创建一个子类对象,把它当作父类来看待使用 类似于自动类型转换: double num = 100; int --> double
向上转型一定是安全的从小范围转向了大范围 弊端: 无法调用子类原本特有的内容!因为无论是调用子类的成员方法还是成员变量,根据多态口诀,编译都要先看左(参见继承),而左边的父类没有子类特有的内容,无法调用,编译就会报错!
对象的向下转型
其实是一个还原的动作 子类名称 对象名 = (子类名称)父类对象;
将父类对象,还原成为本来的子类对象。
1 | // 注意,只能还原成本来的子类,不能还原成父类的其他的子类 |
必须保证对象本来是什么,就向下转型成什么,不能转型成别的!编译会通过,但是运行会出现异常 java.lang.ClassCastException
类似于强制类型转换: int num = (int) 10.0;// 可以 int num = (int) 10.5; //不可以 精度损失
instanceof关键字
对象 instanceof 类名称
返回值 boolean
,判断前面的对象能不能当作后面类型的实例
上述记录与2019.10.31
final关键字
- 用来修饰一个类
1 | public final class 类名称 { |
当前这个类不能有任何子类。(太监类) 但是一定有父类(Object
) 其中的所有成员方法,都无法进行覆盖重写,但可以对其父类的方法做覆盖重写。
- 修饰一个方法 这个方法就是最终方法(不能被覆盖重写)
1 | 修饰符 final 返回值类型 方法名称(参数列表) { |
对于类和方法来说,abstract
关键字(一定要覆盖重写)和final
关键字(不能覆盖重写)不能同时使用,产生矛盾
修饰一个局部变量 一旦使用
final
来修饰局部变量,这个变量就不能进行更改,并且必须赋予初始值。 一次赋值,终生不变 对于基本类型来说,不可变说的是变量当中的数值不可改变 对于引用类型来说,不可变说的是变量当中的地址值不可改变,但是地址对应的内容可以改变修饰一个成员变量 这个变量照样不可变 由于成员变量具有默认值,所以用了
final
之后,必须手动赋值,不会再给默认值了。 要么使用直接赋值,要么通过构造方法赋值。二者选其一。 当选构造方法赋值时:必须保证类当中所有重载的构造方法,都最终会对final
修饰的成员变量赋值
权限修饰符
四种权限修饰符: public > protected > (default) > private 同一个类 yes yes yes yes 同一个包 yes yes yes no 不同包子类 yes yes no no 不同包非子类 yes no no no
内部类
一个类内部包含另一个类 内部类与外部类并不是继承关系 例如人体和心脏,汽车和发动机的关系
分类: 1. 成员内部类 2. 局部内部类(包含匿名内部类)
成员内部类
定义格式:
1 | 修饰符 class 外部类名称 { |
内用外,可以随意访问,不受权限修饰符影响 外用内,需要借助内部类对象。
成员内部类文件格式: 编译之前,内部类是写在外部类的.java
文件中,编译之后与外部类分开,单独生成一个外部类名称$内部类名称.class
文件,这也是为什么之前标识符提到不推荐用$
命名类,就是为了这里避免混淆。
使用内部类的两种方式: 1. 间接方式:在外部类的方法当中使用内部类;然后通过 main
调用外部类的方法,从而使用了内部类。 2. 直接方式:外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称();
如果出现了重名现象,那么访问外部类成员变量的格式: 外部类名称.this.外部类成员变量
1 | public class Outer { |
局部内部类
一个类定义在一个方法内部,只有当前所属的方法才能使用它,出了方法外面就不能用了
定义格式:
1 | 修饰符 class 外部类名称 { |
根据类选择权限修饰符规则: 1. 外部类:public / (default)
2. 成员内部类:public / protected / (default) / private
3. 局部内部类:什么都不能写 4. 如果希望局部内部类访问所在方法的局部变量,那么这个局部变量必须是 final
的。从Java 8 开始,只要局部变量实时不变,那么 final
关键字可以省略。
匿名内部类
如果接口的实现类(或者是父类的子类) 只需要使用唯一的一次 那么这种情况下可以省略掉该类的定义,改为使用匿名内部类 定义格式:
1 | // 注意右边,这里不是在new接口,接口是不能直接new对象的,这里是省略的实现接口的类的名字!其实就是匿名类是在创建对象的同时进行了定义,因此只能用一次。 |
对格式进行解析 new 接口名称() {...}
1. new
代表创建对象的动作 2. 接口名称就是匿名内部类需要实现哪个接口 3. {...}
这才是匿名内部类的内容
匿名类和匿名对象(参见12.10 匿名对象)不是一个东西!
- 匿名内部类, 在创建对象的时候,只能使用唯一一次,如果希望多次创建对象,而且类的内容一样的话,那么就必须使用单独定义的实现类了。
- 匿名对象,在调用方法的时候,只能调用唯一一次,如果希望多次调用方法,那么必须给对象取个名字
- 匿名内部类是省略了实现类/子类名称,但是匿名对象是省略了对象名称
上述记录于2019.11.1
常用API(二)
Object类
toString方法: 返回该对象的字符串表示。 默认打印对象的地址值 直接打印对象的名字,其实就是调用对象的toString
方法 判断一个类的toString
方法是否被覆盖重写,直接打印对象System.out.println(该对象)
,如果得到地址值,则没被重写,否则被覆盖重写。 例如:Random
类的toString
没被重写,Scanner
和 ArrayList
的被覆盖重写了
equals方法: 指示其他某个对象是否与此对象“相等” 源代码:
1 | public boolean equals(Object obj) { |
其源码里使用的是==
比较运算符 基本数据类型:比较的是值 引用数据类型:比较的是两个对象的地址值
如果要覆盖重写 equals
方法,要注意的是,由于多态Object
是无法使用子类的成员变量的。所以我们需要一个向下转型,将Object
转换成想要的子类。
举一个字符串重写了equals
方法的例子
1 | public class Demo01Equals { |
浅拷贝
Objects类
JDK7以后添加的一个工具类,它提供了一些方法来操作对象,由一些静态方法组成,这些方法是
null-save
(空指针安全的)或null-tolerant
(容忍空指针的),用域计算对象的hascode
、返回对象的字符串表现形式、比较两个对象。
equals方法: 比较两个对象的时候,Object
类的equals
方法容易抛出空指针异常,而Objes
类中的equals
方法就优化了这个问题:
1 | // Objects.equals源码 |
Date类
java.util.Date
类表示特定的瞬间,精确到毫秒 毫秒值的作用:可以对时间和日期进行计算--把字符串格式的日期转换为毫秒进行计算 时间原点:1970 年 1 月 1 日 00:00:00 GMT (英国格林威治时间) 1000毫秒 = 1秒 中国属于东八区,会把时间增加8个小时
DateFormat类
java.text.DateFormat
是日期/时间格式化子类的抽象类 > 日期/时间格式化子类(如 SimpleDateFormat
)允许进行格式化(也就是日期 -> 文本)、解析(文本-> 日期)和标准化。
成员方法: - String format(Date date)
将一个 Date
格式化为日期/时间字符串。 - Date parse(String source)
从给定字符串的开始解析文本,以生成一个日期。 由于是抽象类所以使用其子类:SimpleDateFormat
它的有参构造需要传入一个字符串类型构造格式,具体构造格式参见API文档,构造格式中的字母有其具体含义不能更改,但是连接字母的符号可以更改。
上述记录于2019.11.4
Calendar类
是一个抽象类,里面提供了很多操作日历字段的方法(YEAR、MONTH、DAY_OF_MONTH、HOUR
等)无法直接创建对象,使用下面的静态方法 Calendar rightNow = Calendar.getInstance();
使用默认时区和语言环境获得一个日历。(得到的是子类对象 GregorianCalendar
, 通过多态用Calendar
接收) 其他常用方法参见API文档
System类
java.lang.System
类中提供了大量的静态方法,可以获取与系统相关的信息或系统级操作。
1 | public static long currentTimeMillis(); // 返回以毫秒为单位的当前时间 可以用来测试程序得效率 |
其他详情方法参见API文档
StringBuilder类
String
的底层是一个被final
修饰的byte
数组,不能改变,所以进行字符串的相加时,内存中就会有多个字符串(也就是多个数组),占用空间多,效率低下! StringBuilder
(字符串缓冲区),可以提高字符串的操作效率(看成一个长度可以变化的字符串) 底层也是一个数组,但是没有被final
修饰,可以改变长度! StringBuilder
在内存中始终是一个数组(初始容量16)如果超出容量,自动扩容,扩大一倍(32) 两个方法:StringBuilder append(); String toString();
详情见API文档
包装类
概念
基本数据类型,使用起来非常方便。但是没有对应的方法,来操作这些基本类型的数据,可以使用一个类把基本类型的数据装起来,在类中定义一些方法,这个类叫做包装类,可以使用类中的方法,来操作这些基本类型的数据。
基本类型 | 对应的包装类(位于java.lang包中) |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
装箱与拆箱
- 装箱:基本类型 --> 包装类对象
- 拆箱:包装类对象 --> 基本类型
1 | package cn.perdant.day12.demo05; |
自动拆箱与自动装箱
JDK1.5之后:基本类型的数据和包装类之间可以自动相互转换 包装类无法直接参与运算,但是可以自动转换为基本数据类型,再进行计算 ps:那我他妈的上面白写了???
基本类型与字符串之间的相互转换
基本类型 --> 字符串 1. 基本类型的值+""(空字符串)
最简单方法 2. 包装类的静态方法 toString(参数)
,不是 Object
类的 toString()
,后者是空参数,所以二者是方法重载的关系 3. String
类的静态方法 valueOf(参数)
字符串 --> 基本类型 包装类的静态方法 parseXXX("数值类型的字符串")
单列集合Collection
集合概述
集合是java中提供的一种容器,可以用来存储多个数据。
集合和数组的区别: 1. 数组的长度是固定的。集合的长度是可变的。 2. 数组中存储的是同一数据类型(基本or引用)的元素;集合存储的都是对象(基本类型自动装箱),而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。
Collection集合框架
两种单列集合:
- List 接口
- 有序的集合(存储和取出元素顺序相同)
- 允许存储重复的元素
- 有索引,可以使用普通的for循环遍历
- Set 接口
- 不允许存储重复元素
- 没有索引(不能使用普通的for循环遍历)
- 子类TreeSet是无序集合
- 子类HashSet是有序集合
Collection 共性方法 常用功能
java.util.Collection
是所有单列集合的父接口,因此在Collection中定义了单列集合(List和Set)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:
public boolean add(E e)
: 把给定的对象添加到当前集合中 。public void clear()
:清空集合中所有的元素。public boolean remove(E e)
: 把给定的对象在当前集合中删除。注意:List接口覆盖重写了此方法!(因为List都有索引,所以重写成了删除索引位置元素)public boolean contains(E e)
: 判断当前集合中是否包含给定的对象。public boolean isEmpty()
: 判断当前集合是否为空。public int size()
: 返回集合中元素的个数。public Object[] toArray()
: 把集合中的元素,存储到数组中。
上述记录于2019.11.5
Iterator 接口
迭代器功能介绍
每种集合的存取方式不同,需要一种通用的获取集合的方式----迭代。 Iterator
迭代器是一个接口,无法直接使用,需要使用其实现类,获取实现类的方式比较特殊:Collection
接口中有一个方法,iterator()
返回的就是迭代器的实现类对象。
1 | /* |
增强for循环
1 | import java.util.ArrayList; |
泛型
概念
是一种未知的数据类型,当我们不知道使用什么数据类型的时候,可以使用泛型 泛型也可以看成是一个变量,用来接收数据类型 Ee: Element
元素 Tt: Type
类型 ArrayList
集合在定义的时候:public class ArrayList<E>{...}
,不知道集合中都会存储什么类型的数据,所以类型使用泛型E
创建对象的时候,就会确定泛型的数据类型 ArrayList<String> list = new ArrayList<>();
会把数据类型作为参数传递,赋值给泛型 E
。此时 E
被确定为 String
1 | package cn.perdant.day13.demo02; |
定义和使用含有泛型的类
1 | package cn.perdant.day13.demo02; |
1 | package cn.perdant.day13.demo02; |
含有泛型的方法
1 | package cn.perdant.day13.demo02; |
1 | package cn.perdant.day13.demo02; |
含有泛型的接口
1 | package cn.perdant.day13.demo02; |
1 | package cn.perdant.day13.demo02; |
1 | package cn.perdant.day13.demo02; |
1 | package cn.perdant.day13.demo02; |
泛型通配符
1 | package cn.perdant.day13.demo02; |
泛型的上限限定: ? extends E
代表使用的泛型只能是E类型的子类/本身 泛型的下限限定: ? super E
代表使用的泛型只能是E类型的父类/本身 平时用的不多
数据结构
本模块只介绍与集合相关的部分,更多内容请另看《数据结构》
栈 LIFO 后进先出
队列 FIFO 先进先出
数组 查询快(数组地址连续,通过首地址找到数组,通过索引快速找到某一个元素) 增删慢(数组长度固定,想要增加或者删除元素,必须创建一个新数组。把原数组复制过来,垃圾回收原数组)
链表 与数组相反: 查询慢(链表地址不连续,每次查询元素,必须从头开始查询) 增删快(链表接口增加/删除元素,对链表的整体结构(指针域和数据域)没有影响,只需要改变指针)
红黑树: 趋近于平衡树(左右叶子节点相等) 查询速度非常快 查询叶子节点最大次数和最小次数不能超过2倍(换句话说 就是趋近于平衡树的不平衡树) 约束规则: 1.节点可以是红色的或者黑色的 2.根节点是黑色的 3.叶子节点(空节点)是黑色的 4.每个红色的节点的子节点都是黑色的 5.任何一个节点到其每一个叶子节点的所有路径上的黑色节点数相同
接口List
有序、有索引(注意索引越界异常)、允许存储重复的元素
ArrayList 本质是一个数组结构,查找快、增删慢。(其中的add方法调用的copyof) 上文使用的ArrayList就实现了List接口,这里就不过多赘述了。 不同步 速度快
LinkedList 本质是链表结构,查询慢,增删快。 里面包含了大量操作首尾元素的特有方法(属于其特有的,不能通过多态调用) 不同步
Vector 所有单列集合的祖宗,底层也是数组 同步 速度慢 了解即可
上述记录于2019.11.6
接口Set
不允许存储重复元素、没有索引 因此没有带索引的方法,不能用普通 for
循环遍历,里面方法和 Collection
方法一致,不过多赘述,毕竟 Collection
是他的父接口啊。。。。
HashSet
java.util.HashSet
实现了Set
接口,是一个无序的集合(存储元素和取出元素的顺序有可能不一致),重复元素只存一次;底层是一个哈希表结构(查询的速度非常的快)
1 | package cn.perdant.day13.demo05; |
哈希值
1 | package cn.perdant.day13.demo05; |
哈希表
哈希表是 HashSet
的存储结构 JDK1.8之前 哈希表 = 数组+链表
JDK1.8之后 哈希表 = 数组+链表
如果链表长度超过8位:哈希表 = 数组+红黑树
(提高查询的速度) 数组里存hashCode
,链表/树里存元素
Set集合不允许重复元素
Set
集合在调用add
方法的时候,add
会调用元素的hashCode
方法和equals
方法,判断元素是否重复。hashCode
相同时,调用equals
看两个元素是否相等(此处的相等不一样的类型有不一样的意思,比如字符串相等是指两个字符串的字符是一样的,所以你自定义的类型相等是要看你自己怎么定义,所以要重写equals)。因此如果要用Set
存储自定义类型元素的话,必须重写hashCode
方法和equals
方法。
HashSet存储自定义类型元素
必须重写HashCode和equals方法 保证元素不重复!
1 | package cn.perdant.day13.demo06; |
1 | package cn.perdant.day13.demo06; |
LinkedHashSet
HashSet
的子类 是有序的! 底层是一个哈希表+链表(用来记录元素的存储顺序,保证元素有序)
可变参数
在JDK1.5以后,如果我们定义一个方法需要接收多个参数,并且多个参数类型一致,我们可以对其简化成如下格式
1 | 修饰符 返回值类型 方法名(参数类型... 形参名){} |
原理: 可变参数底层就是一个数组,根据传递参数个数不同,会创建不同长度的数组,来存储这些参数 传递的参数个数,可以是0个(不传递),1, 2...多个。 传递几个参数,数组的长度就是几。
注意事项: 1. 一个方法的参数列表,只能有一个可变参数 2. 如果方法的参数有多个,那么可变参数必须写在参数列表的末尾
可变参数的终极写法
1 | 修饰符 返回值类型 方法名(Object... 形参名){} |
Collections类
集合工具类,用来对集合进行操作
常用静态方法: 1. addAll
一次性添加多个元素、 2. shuffle
打乱集合顺序、 3. sort
排序(只能传List集合参数,默认升序,对于自定义类,需要实现Comparable接口,覆盖重写compareTo(参数)方法才能使用sort) Comparable
接口的排序规则:自己(this)-参数
升序, 反之降序。 4. sort(ListM<T> list, Comparator<? super T>)
按照给定规则进行排序 Comparator
是一个接口,里面有compare(o1, o2)
方法,需要覆盖重写 Comparator
接口的排序规则:o1-o2
升序,反之降序。
1 | package cn.perdant.day13.demo07; |
1 | package cn.perdant.day13.demo07; |
上述记录于2019.11.7
双列集合Map
Map特点:
- 不同于
Collection
,Map<K,V>
集合是一个双列集合,一个元素包含两个值(一个key,一个value) - key和value的数据类型可以相同,也可以不同
- key是不允许重复的,value是可以重复的
- key和value是一一对应的
HashMap
java.util.HashMap
实现Map接口。 底层和HashSet
一样是哈希表结构(查询速度非常快),无序 不同步(多线程 速度快)
LinkedHashMap
HashMap的子类,底层是哈希表+链表。类似LinkedHashSet,有序(因为加了链表)
Map常用方法
Map接口中定义了很多方法,常用的如下: - public V put(K key, V value)
: 把指定的键与指定的值添加到Map集合中。若key
不存在,返回值为null
;否则,返回参数value
key不存在时,返回的是value的默认值,因为value是T类型,也就是引用类型,引用类型的默认值就是null
public V remove(Object key)
: 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。若key
存在,返回key
对应的value
;否则,返回null
public V get(Object key)
根据指定的key
键,在Map集合中获取对应的value
值。boolean containsKey(Object key)
判断集合中是否包含指定的键。public Set<K> keySet()
: 获取Map集合中所有的键,存储到Set集合中。public Set<Map.Entry<K,V>> entrySet()
: 获取到Map集合中所有的键值对对象的集合(Set集合)。
1 | package cn.perdant.day14.demo01; |
1 | package cn.perdant.day14.demo01; |
遍历Map集合
遍历方法: 1. keySet
: 把Map
集合中所有的key
取出来存储到Set
集合中,这样就可以通过遍历Set
遍历key
,通过get
方法遍历value
。 2. entrySet
: 把Map
集合内部得多个Entry
对象取出来,存储到一个Set
集合中,遍历Set
集合,获取Set
集合中的每一个Entry
对象。再使用Entry
对象中的方法getKey
和getValue
方法。
1 | package cn.perdant.demo02; |
Entry键值对对象
Entry
是 Map
的嵌套类(内部类) 作用:当Map
集合一创建,就会在Map
集合中创建一个Entry
对象,用来记录键与值(键值对对象,键与值的关系)
HashMap存储自定义类型键值
类似 31.5 HashSet存储自定义类型元素 为了保证key
值唯一,作为key
元素的类型,必须覆盖重写hashCode
方法和equals
方法!
LinkedHashMap
继承了HashMap
集合 底层原理:哈希表+链表结构(记录元素的顺序)
1 | package cn.perdant.day14.demo02; |
HashTable
JDK1.0
就有了,最早期的集合 底层也是哈希表 键与值都不允许存储null
(之前学的所有集合都可以),同步 线程安全 单线程,速度慢 和Vector
一样,都被1.2版本之后的HashMap
取代了 但是其子类Properties
依然活跃 ,Properties
是唯一一个和IO
流相结合的集合
JDK9优化
Set Map List
三个接口中都定义了新的方法 of
1 | package cn.perdant.day14.demo04; |
Debug追踪
可以让代码逐行执行,查看代码的过程,调式程序中出现的bug
使用方式: 1. 在行号的右边,鼠标左键单击,添加断点(初学时添加在每个方法的第一行,熟练了之后 哪里有bug添加到哪里) 2. 右键,选择Debug执行程序 3. 程序就会停留在添加的第一个断点处
执行程序: f8
:逐行执行程序 f7
:进入到方法中 shift+f8
:跳出方法 f9
:跳到下一个断点,如果没有下一个断点,那么就结束程序 ctrl+f2
:退出debug模式,停止程序 Console
:切换到控制台
上述记录于2019.11.8
异常
异常的概念
异常指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。 在java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出一个异常对象。Java处理异常的方式是中断处理。 异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行!
异常的体系
异常的根类java.lang.Throwable
其下有两个子类:java.lang.Error
与 java.lang.Exception
Error
是错误:错误就相当于程序得了一个无法治愈的病,必须修改源代码,程序才能继续执行。Exception
是编译期异常,进行编译(写代码)时Java程序出现的问题。这种异常在写代码的时候编译器就会指出来。例如:java.io.IOException
- 其子类
RuntimeException:
是运行期异常,java程序运行过程中出现的问题。这种异常在写代码的时候编译器不会指出来,但是如果不处理的话,编译运行的时候会产生异常对象,并且默认向上抛出,直到抛给JVM
,Java虚拟机的处理方式是:打印异常信息、终止当前正在执行的Java程序。(看不懂这段话没关系,看完下面的图,再回来看就理解了。) - 异常就相当于程序得了一个小毛病,把异常处理掉,程序可以继续执行。
- 其子类
- 简而言之:编译期异常必须处理,使用try...catch或throws;运行期异常可不处理,默认交给JVM处理
异常产生过程的解析
分析异常是怎么产生的,如何处理异常
异常的处理
五个关键字try catch finally throw throws
抛出异常 throw
1 | package cn.perdant.day15.demo01; |
Objects非空判断
之前提到过Objects类提供一些静态方法,这些方法是空指针安全的或容忍空指针的,因为它对对象为null的值进行了抛出异常操作。 public static <T> T requireNonNull(T obj)
: 查看指定引用对象是不是null
1 | public static <T> T requireNonNull(T obj) { |
throws关键字 异常处理的第一种方式
1 | package cn.perdant.day15.demo01; |
try... catch关键字 异常处理的第二种方式
各人见解: try..catch
比 throws
好的地方在于,throws
如果一直向上抛出,抛给虚拟机之后,虚拟机会直接终止程序运行,而有时候我们遇到异常,并不希望整个程序停止,利用try...catch
我们可以合理的控制异常的大小,什么样的异常需要终止程序,什么样的异常可以忽略,什么样的异常可以做一些特殊的操作。比throws
灵活很多。
1 | package cn.perdant.day15.demo01; |
上述记录于2019.11.12
上面的代码用到了Throwable类中定义的3个 异常处理的方法:
String getMessage()
返回 throwable 的简短信息
String toString()
返回 throwable 的详细信息
void printStackTrace()
JVM打印异常对象,默认此方法,打印的异常信息是最全面的
finally代码块
对于try
块里面的代码,发生异常后 ,后面的代码是执行不到的。我们可以把这些代码放到finally
当中。
1 | package cn.perdant.day15.demo01; |
各人见解: 之前说过,try..catch
的一个好处在于,无论是否发生异常,都不影响后面代码的执行,那么写不写finally
的区别在哪呢??我自己试了一下后,才理解了这一节一开始的那一句话:
对于
try
块里面的代码,发生异常后 ,后面的代码是执行不到的。我们可以把这些代码放到finally
当中。
先看一个例题:
1 | public class Demo01Exception { |
在上面的代码运行时,实际执行顺序是在return
之前执行了finally
中的输出语句。 如果不用finally
只是单单写在try...catch
后面的语句是不会执行的,因为在try里已经return
了,这就是所说的无论如何,finally
里的代码一定会执行。 但是,还有两点需要注意,看下面的例子。 第一点:
1 | public class Demo01Exception { |
如果按照前面所说的逻辑,为什么不是输出12 12呢?这里涉及到了JVM的深层原理,我还没学到。。。大体意思就是,return
里面的值和变量i
里面的值 是分开存的,所以finally
里面虽然修改了i
,但是return
里面的值没有变,所以最后返回的仍是2。 第二点:
1 | public class Demo01Exception { |
这就是为什么finally
块中不要有return
,如果有的话,程序不论是否发生异常,都会执行finally
中的语句,最后必定返回的是finally
中的结果。
异常的注意事项
1 | package cn.perdant.day15.demo01; |
- 如果finally有return语句,永远返回finally中的结果,避免该情况!(finally中是一定会执行的代码)
- 如果父类抛出多个异常,子类重写父类方法时,抛出和父类相同的异常或者是抛出父类抛出的异常的子类或者不抛出异常。
- 父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出,简而言之:父类异常是什么样,子类异常就是什么样
- 在
try...catch
后可以追加finally
代码块,其中的代码一定会被执行,通常用于资源回收
1 | package cn.perdant.day15.demo01; |
自定义异常
1 | package cn.perdant.day15.demo01; |
多线程
并发与并行
并发:指两个或多个事件在同一个时间段内发生 并行:指两个或多个事件在同一时刻发生(同时发生)
线程和进程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 线程: 是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
进程:
线程:
线程的调度 分时调度:轮流使用cpu 抢占式调度:谁的优先级高,谁来抢占cpu (java使用的此种调度)
各人思考: 一个程序有多个进程,一个进程有多个线程, 线程和cpu是什么关系?网上找了些资料,先说结论:多线程和cpu有几个核没关系,单核也可以开多线程,cpu在多个线程间来回切换,多核开多线程的优势在于速度更快罢了。
下面放上我在网上找的摘录: >1. CPU和线程的关系 >(1)第一阶段,单CPU时代,单CPU在同一时间点,只能执行单一线程。比如,的某一刻00:00:00 这一秒,只计算1+1=2(假设cpu每秒计算一次) >(2)第二阶段,单CPU多任务阶段,计算机在同一时间点,并行执行多个线程。但这并非真正意义上的同时执行,而是多个任务共享一个CPU,操作系统协调CPU在某个时间点,执行某个线程,因为CPU在线程之间切换比较快,给人的感觉,就好像多个任务在同时运行。比如,电脑开了两个程序qq和qq音乐,假设这两个程序都只有一个线程。人能够感觉到CPU切换的频率是一秒一次,假设当前cpu计算速度是1秒1次,那么我们就能明显感到卡顿,当聊天,点击发送按钮时候,qq音乐就会停止运行。当前cpu计算速度是1秒100次,也就是它能在一秒之内在这两个进程见切换100次,那么我们就感不到卡顿,觉得QQ和QQ音乐是同时在运行。 >(3)第三阶段,多CPU多任务阶段,真正实现的,在同一时间点运行多个线程。具体到哪个线程在哪个CPU执行,这就跟操作系统和CPU本身的设计有关了。 >1. 举例说明 >(1)假设一种极端情况,一台单核计算机,只运行2个程序A和B。 >假设A和B的优先级相同,A有3个线程,B有1个线程,那么CPU分配给A和B的执行时间应该是3:1。 >(2)假设同一种情况发生在一台多核计算机,核1处理A和B各一个线程,核2处理A剩下的线程。 >(3)刚才说的是线程只消耗CPU,在实际应用中这种情况是不存在的,程序总会跟资源打交道,比如读个文件,查询数据库,访问网络,这个时候多线程才能体现出优势。在一个进程中,让A先用一下CPU去查询数据库,在A查询数据库的时候CPU空闲,B就用一CPU去读文件,让C去访问网络。相对于查询数据库,读取文件这些操作来说,CPU的计算时间几乎可以忽略不计。所以,多线程,实际上是计算机多种资源的并行运用,跟CPU有几个核心没什么关系。
主线程:
1 | package cn.perdant.day15.demo02; |
方式一:创建线程类
Java使用java.lang.Thread
类代表线程,所有的线程对象都必须是Thread
类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流(即一段顺序执行的代码)。Java使用线程执行体来代表这段程序流。
1 | package cn.perdant.day15.demo03; |
1 | // 1. 创建一个Thread类的子类 |
多线程原理:
多线程内存图解:
上述记录于2019.11.13
Thread类
常用方法
currentThread()
getName()
setName()
sleep()
1 | package cn.perdant.day15.demo03; |
1 | package cn.perdant.day15.demo03; |
1 | package cn.perdant.day15.demo03; |
方式二:实现Runnable接口
1 | package cn.perdant.day15.demo03; |
1 | package cn.perdant.day15.demo03; |
以后多线程,尽量使用实现Runnable接口的方式
匿名内部类方式实现线程创建
1 | package cn.perdant.day15.demo03; |
线程安全
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题概述
线程安全问题示例代码:
1 | package cn.perdant.day15.demo04; |
1 | package cn.perdant.day15.demo04; |
运行结果:
线程安全问题产生原理分析:
为了解决解决线程安全问题采用线程同步技术
同步技术的原理: 这张图要结合下面的几种技术的代码例子来理解
技术一:同步代码块
1 | package cn.perdant.day15.demo05; |
1 | package cn.perdant.day15.demo05; |
38.3 技术二:同步方法
1 | package cn.perdant.day15.demo06; |
静态同步方法: 静态同步方法的锁对象不是this,因为this是创建对象之后产生的,静态方法优先于对象 静态方法的锁对象是本类的class属性 --> class文件对象(反射,后面会讲)
技术三:Lock锁技术
JDK1.5后的新接口java.util.concurrent.locks
1 | package cn.perdant.day15.demo07; |
线程状态
计时等待(Timed Waiting)
锁阻塞(Blocked)
无限等待(Waiting)
等待唤醒机制
1 | package cn.perdant.day15.demo08; |
上面的代码示例就是先熟悉一下锁对象调用的两个方法:wait
和 notify
记住!这俩是锁对象调用的! 线程类调用的是start
sleep
currentThread
各人见解: wait
和 notify
进入的这种无线等待模式的好处是什么呢? 之前卖票的例子里我们可以看到,虽然加入了同步技术解决了线程安全问题,但是,线程的运行顺序是没有规则的,谁抢到cpu谁就运行,但是如果我规定,奇数票必须第一个线程买,偶数票第二个线程卖呢?如果使用wait
和notify
就可以让线程一在票数为奇数的时候notify
不是奇数的时候 wait
线程二同理。这样就可以让线程有一个规则顺序执行。(你们可以自己试一下卖票这个题,我自己写了一个,参见39.3 练习)
这也就是下面要讲的:线程间的通信。
线程间通信
概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。 例如前面的包子案例,顾客线程吃包子,老板线程做包子
为什么要处理线程间通信: 多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
如何保证线程间通信有效利用资源: 多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
代码案例:
1 | package cn.perdant.day15.demo09; |
1 | package cn.perdant.day15.demo09; |
1 | package cn.perdant.day15.demo09; |
1 | package cn.perdant.day15.demo09; |
练习
1 | // 票 |
上述记录于1029.11.14
线程池
线程池思想概述
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务? 在Java中可以通过线程池来达到这样的效果。
线程池概念
线程池:其实就是一个容纳多个线程的容器(还记得前面学的容器吗? Collection
Map
),其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
线程池的工作原理:
合理利用线程池能够带来三个好处: 1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。 2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
线程池的使用
Java里面线程池的顶级接口是java.util.concurrent.Executor
,但是严格意义上讲Executor
并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService
。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors
线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors
工程类来创建线程池对象。
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量) 获取到了一个线程池ExecutorService 对象,那么怎么使用呢? 方法如下:public Future<?> submit(Runnable task)
:获取线程池中的某一个线程对象,并执行void shutdown()
关闭/销毁线程池的方法 > Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
使用线程池中线程对象的步骤: 1. 创建线程池对象。 2. 创建Runnable接口子类对象。(task) 3. 提交Runnable接口子类对象。(take task) 4. 关闭线程池(一般不做)。
1 | import java.util.concurrent.ExecutorService; |
1 | // 2. 创建Runnable接口子类对象 |
线程池底层原理:
Lambda 表达式
函数式编程思想概述
在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。
面向对象的思想:做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情 函数式编程思想:只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程
传统写法
1 | package cn.perdant.day15.demo11; |
1 | package cn.perdant.day15.demo11; |
对于Runnable
的匿名内部类用法(详情见 37.7)可以分析出几点内容:
Thread
类需要Runnable
接口作为参数,其中的抽象run
方法是用来指定线程任务内容的核心;- 为了指定
run
的方法体,不得不需要Runnable
接口的实现类; - 为了省去定义一个
RunnableImpl
实现类的麻烦,不得不使用匿名内部类; - 必须覆盖重写抽象
run
方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错; - 而实际上,似乎只有方法体才是关键所在。
编程思想转换
做什么,而不是怎么做
我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而不得不创建一个对象。我们真正希望做的事情是:将run
方法体内的代码传递给Thread
类知晓。
传递一段代码——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。 2014年3月Oracle所发布的Java 8(JDK 1.8)中,加入了Lambda表达式的重量级新特性,为我们打开了新世界的大门。
Lamda写法
1 | package cn.perdant.day15.demo11; |
1 | package demo12; |
1 | package demo12; |
Lambda省略格式
可推导即可省略:
可推导可省略类似之前讲到的泛型,泛型在赋值号右边new出来的泛型可以不用写,因为赋值号左边写了,可以推导出右边的泛型。 Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以根据上下文推导得知的信息,都可以省略。例如上例还可以使用Lambda的省略写法:
1 | public static void main(String[] args) { |
Lambda标准格式的省略规则:
- 小括号内参数的类型可以省略;
- 如果小括号内有且仅有一个参,则小括号可以省略;
- 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。必须一起省略!
Lambda的使用前提
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。
但是使用时有几个问题需要特别注意: 1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。 无论是JDK内置的Runnable
、Comparator
接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。 2. 使用Lambda必须具有上下文推断 也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例 > 备注:有且仅有一个抽象方法的接口,称为“函数式接口”。
上述记录于2019.11.15
File类
概述
java.io.File
类:Java
把电脑中的文件和文件夹(目录)封装为一个File
类,与操作系统无关,我们可以使用File
类对文件和文件夹进行操作:
- 创建一个文件/文件夹
- 删除一个文件/文件夹
- 获取一个文件/文件夹
- 判断文件/文件夹是否存在
- 对文件夹进行遍历
- 获取文件的大小
上述记录于2019.11.19
静态成员变量
File.pathSeparator
: 路径分隔符 在配置环境变量里提到过path
里面就用到了这种分隔符 File.separator
:名称分隔符
代码示例:
1 | import java.io.File; |
路径
绝对路径:是一个完整的路径,以盘符(C:,D:)开始的 相对路径:是一个简化的路径,如果文件存储在与当前项目同一个的根目录下的话,可以不写根目录之前的路径,只写根目录之后的路径。
什么是根目录,比如我后面反复用到的路径
"D:\\IdeaProjects\\basic-code\\..."
这里我写的所有代码都是basic-code
目录下建立的各种module
中,所以这里的根目录是basic-code
,只要是在basic-code
目录下的文件/文件夹,basic-code
之前的路径,包括basic-code
都可以省略。
注意:
- 路径不区分大小写
- 路径中的文件名称分隔符Windows使用反斜杠
\\
,单个反斜杠是转义字符,所以要用两个反斜杠代表一个普通的反斜杠
构造方法
File(String pathname)
参数:字符串的路径名称
- 可以以文件结尾,也可以是文件夹结尾
- 可以是绝对路径,也可以是相对路径
- 可以是存在的,也可以是不存在
创建File对象,只是把字符串路径封装为File对象,不考虑路径的真假情况说白了,你路径可以随便写,只要符合路径定义的格式就可以了,这里编译器是不会去处理判断你的文件是否存在,你写的路径是否正确等等。因为你还没用这个file类实例做任何操作。
File(String parent, String child)
参数:父路径和子路径 好处:可以单独书写,使用起来非常灵活,父子路径都可以变化File(File parent, String child)
参数:父路径是File类型,可以使用File类的方法,对路径进行一些操作,再使用路径创建对象
获取的方法
public String getAbsolutePath()
:返回此File的绝对路径名字符串public String getPath()
:获取构造方法中传递的路径。 构造方法中用的是绝对路径,就返回绝对路径,用的是相对路径,就返回相对路径。toString
的源码调用的就是getPath
方法public String getName()
:获取构造方法传递的路径的结尾部分,就是只返回路径的最后一层的文件/文件夹public long length()
:获取的是构造方法指定的文件的大小,以字节为单位。注意:文件夹是没有大小概念的,不能获取文件夹的大小,方法返回0;如果构造方法中给出的路径不存在,也返回0
判断的方法
public boolean exists()
:用于判断构造方法中的路径是否存在public boolean isDirectory()
:用于判断构造方法中的路径是否以文件夹结尾public boolean isFile()
:与上一方法相反,用于判断构造方法中的路径是否以文件结尾2、3 方法是互斥的,一个返回
true
另一个就返回false
。但是如果使用这两个方法时file
构造时传递的路径是不存在的,都返回false
创建/删除的方法
public boolean createNewFile()
:
- 当且仅当具有该名称的文件尚不存在时,创建一个新的空文件,返回
true
- 如果文件存在或者有同名的文件夹存在,返回
false
- 创建文件的路径和名称,在构造方法中给出(构造方法的参数)
- 如果路径不存在,抛出IOException,因此调用时必须处理异常。
try...catch
orthrows
- 此方法只能创建文件,不能创建文件夹。
public boolean delete()
:
- 删除构造方法路径给出的文件/文件夹
- 文件/文件夹 删除成功 返回
true
- 文件夹中有内容,不会删除,返回
false
- 构造方法中的路径不存在,返回
false
- 此方法直接在硬盘删除文件/文件夹,不走回收站
public boolean mkdir()
:创建单级空文件夹public boolean mkdirs()
:既可以创建单级空文件夹,也可以创建多级空文件夹。对于3、4两种方法:
- 如果文件夹不存在,创建并返回
true
- 如果文件夹存在,返回
false
;如果路径不存在,也返回false
- 只能创建文件夹,不能创建文件。就算你路径里写的
xxx.txt
,创建的照样是一个名字为xxx.txt
的文件夹,这里的.txt
并不是文件后缀,只是当作文件夹名字来处理了
目录的遍历
public String[] list()
:遍历构造方法中给出的目录,会获取目录下所有文件/文件夹的名称,把获取到的文件/文件夹的名称存储到一个String[]
数组里。public File[] listFiles()
:遍历构造方法中给出的目录,会获取目录下所有文件/文件夹的名称,把获取到的文件/文件夹封装为File
对象,存储到一个File[]
数组里。对于1、2两种方法:
- 如果路径不存在或者不是一个合法路径,抛出空指针异常
NullPointerException
- 可以遍历到隐藏的文件/文件夹
递归
概述
递归:指在当前方法内调用自己的这种现象。
递归的分类
递归分为两种:
直接递归:方法自身调用自己。 间接递归:A方法调用B方法,B方法调用C方法,C方法调用A方法。
注意事项
递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出
StackOverflowError
(方法进栈,然后再次调用方法自身,只有进栈,没有出栈。就会导致栈内存中有无数个方法。从而超出栈内存的大小。)在递归中虽然有限定条件,但是递归次数不能太多。否则也会发生栈内存溢出。
构造方法,禁止递归,否则编译报错。(构造方法是创建对象使用的,递归会导致内存中有无数多个对象!)
递归的使用前提:当调用方法的时候,方法的主体不变,每次调用方法的参数不同
使用递归必须明确:
- 递归的结束条件
- 递归的目的
练习
计算1 ~ n的和
实现代码:
1 | public class Demo02Recurison { |
n的阶乘
实现代码:
1 | public class Demo03Recurison { |
递归打印多级目录
如何打印一个目录下的所有文件和文件夹,并且继续打印文件夹里面的文件和文件夹。直到打印到没有文件夹只有文件或空文件夹的底层。
代码实现:
1 | public class Demo04Recurison { |
我们再进一步思考:如果我并不是想仅仅遍历多级目录,而是想在遍历完这些目录之后,只打印.java
结尾的文件呢?其实也简单,去掉打印当前目录的那一步,然后在判断是文件后再加一步检验文件名是否以.java
结尾即可。
代码实现:
1 | import java.io.File; |
文件过滤器
上面这种选择性打印是比较简单的情况,如果遇到复杂的情况呢?如果我们想让这种规则适用于其他的已经写好的遍历文件的代码里呢?这样一次次的修改代码是不是太麻烦了? 这里,我们引入了一种新的接口:文件过滤器FileFilter
和FilenameFilter
在File
类中有两个ListFiles
的重载方法,方法的参数传递的就是过滤器
File[] listFiles(FileFileter filter)
java.io.FileFilter
接口:用于过滤File
对象- 其中的抽象方法:
boolean accept(File pathname)
用来过滤文件的方法 - 参数:
pathname
使用listFiles
遍历目录得到的每一个File
对象
- 其中的抽象方法:
File[] listFiles(FilenameFileter filter)
java.io.FilenameFileter
接口:用于过滤文件名称- 其中的抽象方法:
boolean accept(File dir, String name)
用来过滤文件的方法 - 参数:
dir
:调用listFiles
的File
对象(这道题里就是我们一开始给的要遍历的目录封装成的File
对象);name
:使用listFiles
遍历目录得到的每一个文件/文件夹的名称。
- 其中的抽象方法:
注意:
两个过滤器接口都没有实现类,需要我们自己写实现类,重写过滤的方法accept
,在方法中自己定义过滤的规则。
我们可以把上节的代码使用文件过滤器再一步优化:
代码优化:
1 | import java.io.File; |
1 | import java.io.File; |
文件过滤器原理
看不懂代码的话,可以先看看这张原理图,为方便理解,这里的accept
规则是return true
,就相当于没有过滤,和没加过滤器的效果一样。
终极优化版
结合之前学到的匿名内部类和Lamda表达式 我们可以把代码再一次化简,这里我们加上listFiles
的另一种重载方法的用法。
代码优化:
1 | import java.io.File; |
上述记录于2019.11.20
IO概述
什么是IO
数据的传输,可以看做是一种数据的流动,按照流动的方向,以内存为基准,分为输入input
和输出output
,即流向内存是输入流,流出内存的输出流。
Java
中I/O
操作主要是指使用java.io
包下的内容,进行输入、输出操作。输入也叫做读取数据,输出也叫做作写出数据。
IO的分类
根据数据的流向分为:输入流和输出流。
- 输入流 :把数据从
其他设备
上读取到内存
中的流。 - 输出流 :把数据从
内存
中写出到其他设备
上的流。
格局数据的类型分为:字节流和字符流。
- 字节流 :以字节为单位,读写数据的流。
- 字符流 :以字符为单位,读写数据的流。
顶级父类
输入流 | 输出流 | |
---|---|---|
字节流 | 字节输入流 InputStream |
字节输出流 OutputStream |
字符流 | 字符输入流 Reader |
字符输出流 Writer |
字节流
一切皆为字节
一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,8位二进制数就是一个字节,传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。
字节输出流 OutputStream
java.io.OutputStream
抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的共性方法。
public void close()
:关闭此输出流并释放与此流相关联的任何系统资源。public void flush()
:刷新此输出流并强制任何缓冲的输出字节被写出。public void write(byte[] b)
:将 b.length字节从指定的字节数组写入此输出流。public void write(byte[] b, int off, int len)
:从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。public abstract void write(int b)
:将指定的字节输出流。
文件字节输出流 FileOutputStream
OutputStream
有很多子类,下面介绍其最简单的一个子类java.io.FileOutputStream
文件输出流,用于将数据写出到文件。
输出数据的原理:
java程序 --> JVM
(虚拟机)--> OS
(操作系统)--> OS
调用写数据的方法,把数据写入到文件中
JVM
起到一个中介作用,因为不一样的操作系统,文件的某些特殊字符的使用方式是不同的,比如后面要讲的结束符,换行符等等,所以先要将数据交给JVM
,JVM
将数据处理成OS
可以接收的形式,然后OS
再与文件做交互,将数据写入文件
构造方法
public FileOutputStream(File file)
public FileOutputStream(String name)
- 参数: 写入数据的目的地
String name
:目的地是一个文件的路径File file
:目的地是一个文件
- 参数: 写入数据的目的地
构造方法的作用:
- 创建一个
FileOutputStream
对象 - 会根据构造方法中传递的文件/文件路径,创建一个空的文件
- 会把
FileOutputStream
对象指向创建好的文件
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会用写入后的文件覆盖原来的文件。
写出字节数据
字节输出流的使用步骤:
- 创建一个
FileOutputStream
对象,构造方法中传入数据的目的地 - 调用
FileOutputStream
对象中的方法write
,把数据写入到文件中 - 释放资源(流使用会占用一定的内存,使用完毕要把内存清空,提高程序的效率)
写出字节方法:
void write(int b)
,写出一个字节数据void write(byte[] b)
,写出数组中的数据
- 也可以用
String
类的getBytes()
方法,将String
类型解码成字节,getBytes()
默认使用UTF-8
的编码表。那我们打开文件想看到我们写的内容,也是必须用UTF-8
编码格式打开,不然会乱码。后面转换流章节会详细解释
其实打开文件文本编辑器会将字节转换成字符,这步一般的文本编辑器都会自动根据文件内的字节来找到对应的编码表??
write(byte[] b, int off, int len)
, 写出数组中的部分数据- 参数:
int off
:数组的开始索引int len
:写几个字节
- 参数:
代码示例:
1 | import java.io.FileOutputStream; |
追加续写
经过以上的演示,每次程序运行,创建输出流对象,都会清空目标文件中的数据。如何保留目标文件中数据,还能继续添加新数据呢?使用两个参数的构造方法
public FileOutputStream(File file, boolean append)
public FileOutputStream(String name, boolean append)
参数: String name,File file
:写入数据的目的地 boolean append
:追加写开关
true
:创建对象不会覆盖原文件,继续在文件的末尾追加写数据false
:创建一个新文件,覆盖原文件
写出换行
使用换行符号:回车符 \r
和换行符号 \n
回车符:回到一行开头(return) 换行符:下一行(newline) 不同操作系统使用的方法不同。 - Windows: \r\n
- Linux: /n
- Mac: /r
但是从 Mac OS X 后开始与Linux统一
结合续写,代码示例:
1 | import java.io.FileOutputStream; |
字节输入流 InputStream
java.io.InputStream
抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的共性方法。
public void close()
:关闭此输入流并释放与此流相关联的任何系统资源。public abstract int read()
: 从输入流读取数据的下一个字节。public int read(byte[] b)
: 从输入流中读取一些字节数,并将它们存储到字节数组 b中
文件字节输入流 FileInputStream
仍然是介绍一个子类:java.io.FileInputStream
类是文件输入流,用于从文件中读取字节。
输入数据的原理:
Java
程序 --> JVM
(虚拟机)--> OS
(操作系统)--> OS
调用读数据的方法,把文件的数据读取到内存中
构造方法
FileInputStream(File file)
FileInputStream(String name)
参数:读取文件的数据源 String name
:文件的路径 File file
: 文件
构造方法的作用: 1. 会创建一个FileInputStream
对象 2. 会把FileInputStream
对象指向构造方法中传入的参数的文件
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出FileNotFoundException
读取字节数据
字节输入流的使用步骤:
- 创建
FileInputStream
对象,构造方法中,绑定要读取的数据源 - 使用
FileInputStream
对象中的方法read
,读取文件 - 释放资源
读取字节方法
int read()
:一次读取一个字节,提升为int
型, 读取到文件末尾,返回-1
,int read(byte[] b)
:一次读取多个字节, 返回读取的有效字节个数
代码示例:
1 | import java.io.FileInputStream; |
字节流练习:图片复制
接下来,来一个结合输入输出流的练习。做一个简单的图片赋值,所有的文件都是字节,图片也不例外,我给大家提供一张我老婆的照片,要求:把我老婆的照片从C盘复制到D盘。
新建一个输入流,新建一个输出流。 做一个循环,把每次read
的write
出去。就ok啦。
代码:
1 | import java.io.FileInputStream; |
字符流
当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储(Unicod
三个字节、GBK
两个字节)。所以Java
提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。 里面用到的各种方法使用步骤和字节流相似。
字符输入流 Reader
java.io.Reader
抽象类是表示用于字符输入流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的共性方法。
public void close()
:关闭此流并释放与此流相关联的任何系统资源。public int read()
: 从输入流读取一个字符。public int read(char[] cbuf)
: 从输入流中读取一些字符,并将它们存储到字符数组cbuf
中 。
FileReader类
java.io.FileReader extends InputStreamReader extends Reader
FileReader
文件字符输入流:把硬盘文件中的数据以字符的方式读取到内存中
构造方法
FileReader(File file)
FileReader(String fileName)
参数:读取文件的数据源
String fileName
:文件的路径File file
:一个文件
作用:
- 创建一个
FileReader
对象 - 会把
FileReader
对象指向要读取的文件
使用步骤:
- 创建
FileReader
对象,构造方法中绑定要读取的数据源 - 使用
FileReader
对象中的方法read
读取文件 - 释放资源
类似于FileInputStream
读取字符数据
int read()
int read(char[] cbuf)
这俩方法我就不解释了,和字节输入流的read
方法类似只是获取到的一个是字节,一个是字符。
1 | import java.io.FileReader; |
字符输出流 Writer
java.io.Writer
抽象类是表示用于字符输出流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的共性方法。
void write(int c)
写入单个字符。void write(char[] cbuf)
写入字符数组。abstract void write(char[] cbuf, int off, int len)
写入字符数组的某一部分,off数组的开始索引,len写的字符个数。void write(String str)
写入字符串。void write(String str, int off, int len)
写入字符串的某一部分,off字符串的开始索引,len写的字符个数。void flush()
刷新该流的缓冲。void close()
关闭此流,但要先刷新它。
FileWriter类
java.io.FileWriter extends OutputStreamWriter extends Writer
FileWriter
:文件字符输出流,把内存中的字符数据写入到文件中
构造方法
FileWriter(File file)
FileWriter(String fileName)
参数:写入数据的目的地
String fileName
:文件的路径File file
:文件
作用:
- 创建一个
FileWriter
对象 - 根据构造方法中传递的文件/文件的路径,创建文件
- 会把
FileWriter
对象指向创建好的文件
字符输出流使用步骤:
- 创建一个
FileWriter
对象,构造方法中绑定要写入数据的目的地 - 使用
FileWriter
中的方法write
,把数据写入到内存缓冲区中(字符转换为字节的过程) - 使用
FileWriter
中的方法flush
,把内存缓冲区中的数据,刷新到文件中 - 释放资源(会先把内存缓冲区中的数据刷新到文件中,因此第 3 步有时可以省略)
类似于FileOutputStream
基本写出数据
void write(int b)
:写出单个字符 void write(char[] cbuf)
:写出字符数组 void write(char[] cbuf, int off, int len)
: 写出字符数组的一部分 void write(String str)
:写字符串 void write(String str, int off, int len)
:写字符串的一部分
关闭和刷新
因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是close
方法关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush
方法了。
void flush()
:刷新缓冲区,流对象可以继续使用 void close()
:先刷新缓冲区,然后通知系统释放资源,流对象不可以再被使用了
续写和换行
续写:在构造函数中加参数boolean append
FileWriter(File file, boolean append)
FileWriter(String fileName, boolean append)
换行:使用换行符,详情见字节流
上面三节内容和字节流部分极为相似,所以示例代码我整合着写成一块。
代码示例:
1 | import java.io.FileWriter; |
关于 字符/字节 输入/输出流的思考:
文件字节输出流这个过程就是:内存输入字节 --> 字节写入文件。但是write
传入的是int
型的参数,忽略了高24位,直接写入低8位,为什么不直接传入byte
型的参数呢?为什么这么设计???我搜了一下资料,还真在知乎上找到了相同的问题,(!(https://www.zhihu.com/question/39814712))摘两个有意思的回答:
这个问题要从JVM虚拟机与底层交互的方面去分析,所以我觉得这是Java语言和c语言类型定义导致的问题; 首先看Java中的函数定义:
private native void write(int b, boolean append) throws IOException;
注意:是native
方法 C语言中的定义:int fputc(int ch,FILE *fp)
我们看到两种语言中都是使用int
类型,而不是其它,为什么呢? 1、Java中有byte
类型,C语言中没有 2、为什么不用char
?java中byte
到char
需要一个强制转换,而int
不需要; 3、为什么不用short
? 这个是32位CPU 4字节对齐的问题吧, 4、C语言中整型量和字符量可以通用;所以Java和C语言文件读写的通信,最佳类型就是int
。
大致意思就是,C语言中没有byte
类型,所以用的参数int
,而Java
为了兼容C
语言,做出了妥协
因为
read()
方法会返回 0~255 以及 -1。所以write()
最好是设计成与read()
相匹配的模式。 我们还可以注意到read(byte[])
方法也会返回一个int
代表读取到的字节数,同样也可能返回-1代表已到达流末尾。所以这里不需要使用int[]
,而是可以使用byte[]
。 我想如果当初设计read()
方法时,不是采用返回-1来代表流末尾,而是抛出一个异常EOFException
的话,用byte
也是完全可以的。
我想了下,也是,我read()
返回的不单是byte
啊,如果读取到结束位置,read
返回的会是-1
,所以read
返回的数据范围一定会比byte
大1
,那么我就得把read
返回值设置成范围更大的int
。
而且read(byte[])
这种重载方法也很巧妙,正常来讲,如果我把读取到的字节都存入byte[]
的话,又会出现上面说的,如果读取到结束符呢??那不又要用-1来代替?这又要超byte
的范围了,但是read(byte[])
返回值设计成读取到的有效的字节个数,这就很有意思了,如果读取到结束符,我不存byte[]
数组,而是统计一下读取到结束符前有几个字节数,把它返回,如果一个字节都没有读取到,返回-1
。这样我的传入参数不需要是int[]
而是byte[]
即可!
既然讲到了文件字节输入流的read
,我们就接着编码问题在捋一捋文件字节输入流。读取文件内字节 --> 字节传递给内存。但是,到这一步,你得到的就是一串字节,如果你想在控制台打印文件里到底是什么,就需要将字节再编码为字符。可以使用new String(byte[] bytes)
将字节数组转换成字符串,这一步就要注意了,字符串默认用的UTF-8
的编码表,而文件内的字节呢,是跟文件当初怎么写怎么存的有关。
举个例子,我用GBK
编码格式保存了一个文件,里面存了"你好"
两个字,如果按照上面说的流程,那控制台得到的结果就乱码了。因为字符串构造方法使用的编码(UTF-8
)和源文件的编码(GBK
)不一样。当然也有解决办法,详情见转换流
同理,文件字节输出流的write
,如果我们使用字符串的getBytes()
将字符串转换为字节,默认使用的也是UTF-8
编码。 写出的文件如果用其他编码打开,也会乱码。
IO异常处理
在JDK1.7
之前,我们可以使用try..catch finally
处理流中的异常
1 | import java.io.FileWriter; |
由上面的代码可以看到,try...catch finally
格式处理输入输出流是多么的麻烦,如何优化? JDK7
的新特性,在try
后面增加一个()
,在括号中定义流对象。那么这个流对象的作用域,就在try
中有效,try
中代码执行完毕,会自动把流对象释放,不用写finally
了
JDK9
的新特性,在try
前面可以定义流对象,在try
后边()
中可以引入流对象的名称,在try
代码执行完毕之后,流对象也可以释放掉,不用写finally
拿之前图片复制的代码举例:
1 | import java.io.FileInputStream; |
属性集
概述
java.util.Properties extends Hashtable<k,v> implements Map<k,v>
Properties
类来表示一个持久的属性集。Properties
可保存在流中或从流中加载。该类也被许多Java类使用,比如获取系统属性时,System.getProperties
方法就是返回一个Properties
对象。
Properties
是一个唯一和IO流相结合的集合
- 可以使用
Properties
集合中的方法store
把集合中临时数据,持久化写入到硬盘中存储。 - 可以使用
Properties
集合中的方法load
把硬盘中保存的文件(键值对),读取到集合中使用。 Properties
集合是一个双列集合,key
和value
默认都是字符串。
Properties类
构造方法
public Properties()
:创建一个空的属性列表。
基本的存储方法
Object setProperty(String key, String value)
: 相当于Map集合中的put
String getProperty(String key)
:相当于Map集合中的get
Set<String> stringPropertyNames()
:相当于Map集合中的keySet
与流相关的方法
void store(OutputStream out, String comments)
void store(Writer writer, String comments)
参数: - OutputStream out
: 字节输出流,不能写中文 - Writer writer
:字符输出流,可以写中文 - String comments
:注释,用来解释说明保存的文件是做什么用的,不能使用中文,会产生乱码,默认是Unicode
编码,一般使用""
空字符串
使用步骤: 1. 创建Properties
集合对象,添加数据 2. 创建字节输出流/字符输出流对象,构造方法中绑定要输出的目的地 3. 使用Properties
集合中的方法store
,把集合中的临时数据持久化写入硬盘中存储 4. 释放资源
public void load(InputStream inStream)
public void load(Reader reader)
参数:- InputStream inStream:字节输入流,不能读取含有中文的键值对
- Reader reader:字符输入流,能读取含有中文的键值对
使用步骤: 1. 创建Properties集合对象 2. 使用Properties集合对象中的方法load读取保存键值对的文件 3. 遍历Properties集合
注意: 1. 存储键值对的文件中,键与值默认的连接符号可以使用=
或 空格
2. 存储键值对的文件中,可以使用#
进行注释,被注释的键值对,不会再被读取 3. 存储键值对的文件中,键与值默认都是字符串,不用再加引号
代码示例:
1 | import java.io.FileReader; |
缓冲流
概述
缓冲流,也叫高效流,是对4个基本的FileXxx
流的增强,所以也是4个流,按照数据类型分类:
字节缓冲流:BufferedInputStream
,BufferedOutputStream
字符缓冲流:BufferedReader
,BufferedWriter
缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO
次数,从而提高读写的效率。
字节缓冲输出流
java.io.BufferedOutputStream extends OutputStream
BufferedOutputStream
字节缓冲输出流 继承了父类所有的共性方法
构造方法
public BufferedOutputStream(OutputStream out)
public BufferedOutputStream(OutputStream out, int size)
参数: - OutputStream in
:字节输出流,我们可以传递FileOutputStream
,缓冲流会给FileOutputStream
增加一个缓冲区,提高FileOutputStream的写出效率。 - int size
:指定内部缓冲区的大小,不指定就是默认大小。
写出数据
步骤: 1. 创建一个FileOutputStream
对象,构造方法中绑定要输出的目的地 2. 创建一个BufferedOutputStream
对象,构造方法中传递FileOutputStream
对象 3. 使用BufferedOutputStream
对象中的方法write
,把数据写入到内部缓冲区中 4. 使用BufferedOutputStream
对象中的方法flush
,把内部缓冲区中的数据,刷新到文件中 5. 释放资源(会先调用flush
方法刷新,第 4 步可以省略)
代码示例:
1 | import java.io.BufferedOutputStream; |
字节缓冲输入流
java.io.BufferedInputStream extends InputStream
BufferedInputStream
字节缓冲输入流
构造方法
public BufferedInputStream(InputStream in)
public BufferedInputStream(InputStream in, int size)
参数: - InputStream in
:字节输入流,我们可以传递FileInputStream,缓冲流会给FileInputStream增加一个缓冲区,提高FileInputStream的写出效率。 - int size
:指定内部缓冲区的大小,不指定就是默认大小。
读取数据
步骤: 1. 创建FileInputStream
对象 2. 创建BufferedInputStream
对象,构造方法中传递FileInputStream
对象 3. 使用BufferedInputStream
对象的方法read
4. 释放资源
代码示例:
1 | import java.io.BufferedInputStream; |
字符缓冲输出流
java.io.BufferedWriter extends Writer
BufferedWriter
字节缓冲输出流
构造方法
public BufferedWriter(Writer out)
public BufferedWriter(Writer out, int size)
特有成员方法
public void newLine()
: 写一个行分隔符,会根据不同的操作系统,获取不同的行分隔符。 相比之前要根据不同操作系统写不一样的换行符,这个显然简单得多。 println
方法调用的换行就是用的newLine
写出数据
1 | import java.io.BufferedWriter; |
字符缓冲输入流
java.io.BufferedReader extends Reader
BufferedReader
字节缓冲输入流
构造方法
public BufferedReader(Reader in)
public BufferedReader(Reader in, int size)
特有成员方法
public String readLine()
: 读取一行文本行。读取一行数据。通过下列字符之一即可认为某行已终止:换行('\n'
)、回车('\r'
)或回车后直接跟着换行("\r\n"
)
返回值:包含该行内容的字符串但不包含任何行终止符;如果已达到流末尾,则返回null
读取数据
1 | import java.io.BufferedReader; |
练习:文本排序
请将文本信息恢复顺序。
1 | 3.侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必得裨补阙漏,有所广益。 |
这里要用到正则表达式和
String
的split
方法,现在不会写也没关系。看看步骤思想就好。
代码示例:
1 | import java.io.*; |
转换流
字符编码
计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。
编码:字符(能看懂的)--字节(看不懂的)
解码:字节(看不懂的)-->字符(能看懂的)
字符编码Character Encoding
: 就是一套自然语言的字符与二进制数之间的对应规则。
编码表:生活中文字和计算机中二进制的对应规则
字符集
字符集 Charset
:也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。
计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有ASCII
字符集、GBK
字符集、Unicode
字符集等。
当指定了编码,它所对应的字符集自然就指定了,所以编码才是我们最终要关心的。
- ASCII字符集 :
- ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)。
- 基本的ASCII字符集,使用7位(bits)表示一个字符,共128字符。ASCII的扩展字符集使用8位(bits)表示一个字符,共256字符,方便支持欧洲常用字符。
- ISO-8859-1字符集:
- 拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。
- ISO-8859-1使用单字节编码,兼容ASCII编码。
- GBxxx字符集:
- GB就是国标的意思,是为了显示中文而设计的一套字符集。
- GB2312:简体中文码表。一个小于127的字符的意义与原来相同。但两个大于127的字符连在一起时,就表示一个汉字,这样大约可以组合了包含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。
- GBK:最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等。
- GB18030:最新的中文码表。收录汉字70244个,采用多字节编码,每个字可以由1个、2个或4个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。
- Unicode字符集 :
- Unicode编码系统为表达任意语言的任意字符而设计,是业界的一种标准,也称为统一码、标准万国码。
- 它最多使用4个字节的数字来表达每个字母、符号,或者文字。有三种编码方案,UTF-8、UTF-16和UTF-32。最为常用的UTF-8编码。
- UTF-8编码,可以用来表示Unicode标准中任何字符,它是电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。所以,我们开发Web应用,也要使用UTF-8编码。它使用一至四个字节为每个字符编码,编码规则:
- 128个US-ASCII字符,只需一个字节编码。
- 拉丁文等字符,需要二个字节编码。
- 大部分常用字(含中文),使用三个字节编码。
- 其他极少使用的Unicode辅助字符,使用四字节编码。
编码引出的问题
在IDEA中,使用FileReader
读取项目中的文本文件。由于IDEA的设置,项目中创建的文件都是默认的UTF-8
编码,所以没有任何问题。但是,当读取系统中创建的文本文件,如果我们创建文件时选择的编码是GBK
(也叫做ANSI
)编码,就会出现乱码。
OutputStreamWriter
转换流java.io.OutputStreamWriter extends Writer
,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。
构造方法
OutputStreamWriter(OutputStream out)
OutputStreamWriter(OutputStream out, String charsetName)
参数: - OutputStream out
:字节输出流 - String charsetName
:指定的编码表名称,不区分大小写
步骤: 1. 创建OutputStreamWriter
对象 2. 使用OutputStreamWriter
对象的write
方法 3. 使用OutputStreamWriter
对象的flush
方法 4. 释放资源
指定编码写出
1 | import java.io.*; |
InputStreamReader
java.io.InputStreamReader extends Reader
,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受IDE的默认字符集。
构造方法
InputStreamReader(InputStream in)
InputStreamReader(InputStream in, String charsetName)
注意事项:构造方法中指定的编码表要和文件的编码相同,否则会发生乱码
指定编码读取
1 | import java.io.FileInputStream; |
转换文件编码
将GBK编码的文本文件,转换为UTF-8编码的文本文件
1 | import java.io.*; |
关于转换流的总结
其实你可以把InputStreamReader
理解成FileReader
的plus版本。对比一下二者
FileReader
:读取文件,把文件内的字节以字节为单位传给FileReader
,FileReader
按照默认格式编码成字符,以字符为单位传给内存InputStreamReader
:读取文件,把文件内的字节以字节为单位传给InputStreamReader
,InputStreamReader
按照指定格式编码成字符,以字符为单位传给内存
其实他们二者底层都依靠了FileInputStream
先来读取文件内的字节,但二者在将字节转换为字符这步有所不同。
我们再对比InputStreamWriter
和FileWriter
FileWriter
:写出文件,把内存中的字符传给FileWriter
,FileWriter
按照默认格式解码成字节。以字节为单位传给文件InputStreamWriter
:写出文件,把内存中的字符传给InputStreamWriter
,InputStreamWriter
按照指定格式解码成字节。以字节为单位传给文件
同理,这二者都底层都依靠了FileOutStream
当然,
InputStreamReader
底层不单单可以使用FileInputStream
,任何InputStream
的子类都可以,所以,FileReader
仅仅是InputStreamReader
构造时传入FileInputStream
和默认编码格式
的一种特例。这就是为什么FileReader extends InputStreamReader
序列化
概述
我能不能把对象保存到文件中?不同于普通的写文件读文件,将对象写入文件以后,我们使用特定的读对象方法,返回的既不是字节也不是字符,而是一个对象Object
。这样我们就可以直接使用这个对象的各种成员方法和变量
写对象
Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据
、对象的类型
和对象中存储的属性
等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。
读对象
反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。对象的数据
、对象的类型
和对象中存储的数据
信息,都可以用来在内存中创建对象。
ObjectOutputStream
java.io.ObjectOutputStream extends OutputStream
对象的序列化流 把对象以流的方式写入到文件中保存
构造方法
public ObjectOutputStream(OutputStream out)
特有的成员方法
void writeObject(Object obj)
使用步骤 1. 创建ObjectOutputStream
对象,构造方法中传递字节输出流 2. 使用ObjectOutputStream
对象中的方法writeObject
把对象写入到文件中 3. 释放资源
序列化操作
- 一个对象要想序列化,必须满足两个条件:
- 该类必须实现
java.io.Serializable
接口,Serializable
是一个标记接口,当进行序列化和反序列化的时候,会检测类上是否又这个标记,有就可以序列化和反序列化,没有就会抛出NotSerializableException
- 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用
transient
关键字修饰,后面会介绍何为瞬态。
1 | import java.io.Serializable; |
1 | import java.io.FileOutputStream; |
ObjectInputStream类
ObjectInputStream
对象的反序列化流,把文件中保存的对象,以流的方式读取出来使用。
构造方法
public ObjectInputStream(InputStream in)
特有方法
public final Object readObject()
使用步骤 1. 创建ObjectInputStream
对象 2. 使用ObjectInputStream
对象中的方法readObject
3. 释放资源 4. 使用读取出来的对象(可以直接打印看看)
反序列化操作
- 一个对象要想反序列化,必须满足两个条件:
- 类必须实现
Serializable
接口 - 对于
JVM
可以反序列化对象,它必须是能够找到class
文件的类。如果找不到该类的class
文件,readObject
方法会抛出一个ClassNotFoundException
异常。
1 | import java.io.FileInputStream; |
反序列化遇到的异常
另外,当JVM
反序列化对象时,能找到class
文件,但是class
文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException
异常。
缕一缕上面的过程:
- 先对
Person
类的实例做一个序列化写入Person.txt
文件 - 修改
Person
类 - 对
Person.txt
文件做一个反序列化 - 抛出异常
InvalidClassException
分析产生异常的原因:
当定义好
Person
类之后,Person.java
文件会通过javac
编译成一个Person.class
文件,这时Serializable
接口给需要序列化的类,提供了一个序列版本号。Person.class
会有自己特有的一个序列版本号当
Person
类实例序列化写入Person.txt
文件之后,Person.txt
会生成一个和Person.class
相同的序列版本号修改
Person
类之后,会重新编译新的Person.class
文件。序列版本号也随之改变 > 并不是所有的修改都会让版本序列号改变。我简单的试了一下,修改类中的某些字符串内容不会导致版本号改变,但是修改成员变量的访问权限会导致版本号改变。对
Person.txt
做反序列化时,会比较Person.txt
与Person.class
(已经改变了)文件的序列版本号是否相同,若相同,可以进行反序列化,若不同,抛出异常。
如何解决此问题:
- 我只要让修改后的
Person.class
文件不会改变序列版本号即可。那我们自己给它指定一个不可变的序列版本号可以吗? - 在定义类的时候,我们添加一个成员变量
serialVersionUID
并把它用static final
修饰。
1 | public class Person implements Serializable { |
关于静态 static
静态优先于非静态加载到内存中(静态优先于对象进入到内存中) 所以被static
修饰的成员变量不能被序列化,会保留它的默认值。序列化的都是对象。
关于瞬态 transient
被transient
修饰的成员变量,不能被序列化, 它的作用就是,当你不想序列化对象中的某个特定的成员变量时,使用transient
修饰它。
transient
和static
修饰的成员变量都是不能序列化,但是在解决上面的版本号不一致问题时,不能用transient
只能用static
,说明二者还是有区别的。
详细讨论static与transient的区别
结合代码来分析二者区别:
1 | //简单的整个JOJO类 |
1 | // 序列化 |
1 | // 反序列化 |
先用static
修饰
1 | public class JOJO implements Serializable { |
通过上述序列化反序列化后结果:JOJO{name='承太郎', standName='GoldExperience'}
static
是类的变量,不是对象的,所以在序列化的时候,不会和序列化的变量一起写入文件,但此变量已经早于序列化和反序列化被加载到内存中去了,所以在反序列化的时候,静态变量仍存在,不过是类定义是的初始赋值。
个人见解:
这里再仔细讲一下,我在写demo的时候发现,如果我把序列化和反序列化的代码写道一起,在一起运行的时候,得到的结果是JOJO{name='承太郎', standName='白金之星'}
这是因为,如果你是分开写的,那么在序列化时,static
修饰的变量没有序列化,而是在序列化开始之前就加载到内存的方法区中'GoldExperience'
,但是在序列化时调用了构造方法,并且构造方法中将静态变量修改成'白金之星'
,方法区中的静态变量值已经变了。但是你是分开写的,所以这个程序运行完,内存也已经释放了。再看反序列化
反序列化时,首先重新加载JOJO
类,内存的方法区中的static
修饰的变量仍是类定义的时候赋予的值'GoldExperience'
,然后进行反序列化,读对象,对象中的static
修饰的变量值是不在文件中的,它是在内存的方法区中的,所以去方法区中找,找到了'GoldExperience'
如果把上面序列化和反序列化写在一起的话,可以看到,内存不会有释放重新加载这一步,那么内存方法区里的'GoldExperience'
在序列化的时候就被修改成了'白金之星'
,而反序列化的时候,再去内存方法区找静态变量调用的时候,得到的就是'白金之星'
了
再看用transient
修饰的例子
1 | public class JOJO implements Serializable { |
通过上述序列化反序列化后结果:JOJO{name='承太郎', standName='null'}
transient
修饰的变量不会序列化,是完全不会写入文件又因为它不是静态变量也不会写入方法区,相当于此变量从来未被赋值,所以调用此变量时返回的是默认值null
练习:序列化集合
- 将存有多个自定义对象的集合序列化操作,保存到
list.txt
文件中。 - 反序列化
list.txt
,并遍历集合,打印对象信息。
代码实现:
1 | import java.io.*; |
打印流
概述
平时我们在控制台打印输出,是调用print
方法和println
方法完成的,这两个方法都来自于java.io.PrintStream
类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。
PrintStream
java.io.PrintStream extends OutputStream
(字节)打印流
PrintStream
为其他输出流添加了功能,使他们能够方便地打印各种数据值表示形式
PrintStream
特点:
- 只负责数据的输出,不负责数据的读取
- 永远不会抛出
IOException
但是会抛出FileNotFoundException
- 有特有的方法,
print,println
,参数可以是任意类型的值
构造方法
public PrintStream(String fileName)
public PrintStream(OutputStream out)
public PrintStream(File file)
注意:
- 如果使用继承父类的
write
方法写数据,那么查看数据的时候会查询编码表 97 - a - 如果使用自己特有的
print,println
方法写数据,写的数据原样输出 97 - 97
1 | import java.io.FileNotFoundException; |
改变打印流向
System.out
就是PrintStream
类型的,只不过它的流向是系统规定的,打印在控制台上。不过,既然是流对象,我们就可以玩一个"小把戏",改变它的流向。
使用System.setOut
方法改变输出语句的目的地改为参数中传递的打印流的目的地 static void setOut(PrintStream out)
重新分配“标准”输出流
1 | import java.io.FileNotFoundException; |
上述记录于2019.11.23
网络编程入门
软件结构
C/S结构 :全称为
Client/Server
结构,是指客户端和服务器结构。常见程序有QQ、迅雷等软件。B/S结构 :全称为
Browser/Server
结构,是指浏览器和服务器结构。常见浏览器有谷歌、火狐等。
两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程,就是在一定的协议下,实现两台计算机的通信的程序。
网络通信协议
网络通信协议:通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。
TCP/IP协议: 传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol),是Internet最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求。
TCP/IP协议中的四层分别是应用层、传输层、网络层和链路层,每层分别负责不同的通信功能。
- 链路层:链路层是用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如针对光纤、网线提供的驱动。
- 网络层:网络层是整个TCP/IP协议的核心,它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络。
- 运输层:主要使网络程序进行通信,在进行网络通信时,可以采用TCP协议,也可以采用UDP协议。
- 应用层:主要负责应用程序的协议,例如HTTP协议、FTP协议等。
协议分类
通信的协议还是比较复杂的,java.net
包中包含的类和接口,它们提供低层次的通信细节。我们可以直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信的细节。
java.net
包中提供了两种常见的网络协议的支持:
UDP:用户数据报协议(
User Datagram Protocol
)。UDP
是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。由于使用
UDP
协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输例如视频会议都使用UDP
协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。但是在使用
UDP
协议传送数据时,由于UDP
的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP
协议。数据被限制在64kb以内,超出这个范围就不能发送了。(数据报(Datagram):网络传输的基本单位 )
TCP:传输控制协议 (
Transmission Control Protocol
)。TCP
协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在
TCP
连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”。三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
- 第一次握手,客户端向服务器端发出连接请求,等待服务器确认。
- 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。
- 第三次握手,客户端再次向服务器端发送确认信息,确认连接。
完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,
TCP
协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。
网络编程三要素
协议
- 协议:计算机网络通信必须遵守的规则,已经介绍过了,不再赘述。
IP地址
- IP地址:指互联网协议地址(Internet Protocol Address),俗称
IP
。IP地址用来给一个网络中的计算机设备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”。
IP地址分类
IPv4
:是一个32位的二进制数,通常被分为4个字节,表示成a.b.c.d
的形式,例如192.168.65.100
。其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。IPv6
:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。
为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成ABCD:EF01:2345:6789:ABCD:EF01:2345:6789
,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。
常用命令
- 查看本机IP地址,在控制台输入:
1 | ipconfig |
- 检查网络是否连通,在控制台输入:
1 | ping 空格 IP地址 |
特殊的IP地址
- 本机IP地址:
127.0.0.1
、localhost
这两个都代表当前计算机的ip
端口号
网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?
举个例子,电脑a上的qq连接电脑b上的qq,电脑a通过IP地址找到电脑b,但是b上除了qq还有很多其他软件比如飞秋、MSN等,如果我从电脑a上qq发送的消息传到了MSN上是没有任何意义的。那么我如何精确找到我想要传输到的进程呢?需要给每个进程分配一个端口,从而来区分各个进程。
如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。
端口号:是一个逻辑端口,无法直接看到,可以使用一些软件查看端口号。当我们使用网络软件,一打开,操作系统就会为网络软件分配一个随机得端口号。或者,在打开软件的同时和系统要一个指定得端口号
用两个字节表示的整数,它的取值范围是0~65535。其中,0~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败
常见端口:
- 网络端口:80
- 数据库 mysql:3306 Oracle:1521
- Tomcat服务器:8080
利用协议
+IP地址
+端口号
三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。
TCP通信程序
概述
TCP
通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client
)与服务端(Server
)。是面向连接的通信,客户端和服务器端必须经过3次握手,建立逻辑连接,才能通信安全。
两端通信时步骤:
- 服务器端先启动,服务器端不会主动的请求客户端,必须使用客户端请求服务器端
- 客户端和服务器端就会建立一个逻辑连接,而这个链接中包含一个对象,这个对象就是
IO
对象 - 客户端和服务器端就可以使用
IO
对象进行通信,通信数据不仅仅是字符,所以IO
对象是字节流对象 - 客户端与服务器端进行一次数据交互,需要4个
IO
流对象
在Java中,提供了两个类用于实现TCP通信程序:
- 客户端:
java.net.Socket
类表示。创建Socket
对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。 - 服务端:
java.net.ServerSocket
类表示。创建ServerSocket
对象,相当于开启一个服务,并等待客户端的连接。
服务器端必须明确两件事情: 1. 多个客户端同时和服务器进行交互,服务器必须明确和哪个客户端进行的交互 在服务器端有一个方法,叫accept
客户端获取到请求的客户端对象Socket
2. 多个客户端同时和服务器进行交互,就需要使用多个IO
流对象 服务器是没有IO
流的,服务器可以获取到请求的客户端对象Socket
,使用每个客户端Socket
中提供的IO
流和客户端进行交互: - 服务器使用客户端的字节输入流,读取客户端发送的数据 - 服务器使用客户端的字节输出流,给客户端回写数据 简而言之:服务器使用客户端的流和客户端交互
Socket类
java.net.Socket
类:该类实现客户端套接字
套接字指的是两台设备之间通讯的端点。个人理解:套接字就是 协议+IP地址+端口号 有了这三者,网络通信才能确定连接的两端。
构造方法
public Socket(String host, int port)
:创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host
是null
,则相当于指定地址为回送地址。
参数: String host
:服务器主机的名称/服务器的IP地址 int port
:服务器的端口号
回送地址(127.x.x.x) 是本机回送地址(Loopback Address),主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,立即返回,不进行任何网络传输。
成员方法
public InputStream getInputStream()
: 返回此套接字的输入流。- 如果此
Scoket
具有相关联的通道,则生成的InputStream
的所有操作也关联该通道。 - 关闭生成的
InputStream
也将关闭相关的Socket
- 如果此
public OutputStream getOutputStream()
: 返回此套接字的输出流。- 如果此
Scoket
具有相关联的通道,则生成的OutputStream
的所有操作也关联该通道。 - 关闭生成的
OutputStream
也将关闭相关的Socket
- 如果此
public void close()
:关闭此套接字。- 一旦一个
Socket
被关闭,它不可再使用。 - 关闭此socket也将关闭相关的
InputStream
和OutputStream
- 一旦一个
public void shutdownOutput()
: 禁用此套接字的输出流。- 有时候我不想关闭整个
Socket
,我只想关闭Socket
获取到的输出流。 - 任何先前写出的数据将被发送,随后终止输出流。
- 有时候我不想关闭整个
客户端实现步骤
- 创建一个客户端对象
Socket
,构造方法绑定服务器的IP地址和端口号 - 使用Socket对象中的方法
getOutputStream()
获取网络字节输出流OutputStream
对象 - 使用网络字节输出流
OutputStream
对象中的方法write
,给服务器发送数据 - 使用Socket对象中的方法
getInputStream()
获取网络字节输入流InputStream
对象 - 使用网络字节输入流
InputStream
对象中的方法read
,读取服务器回写的数据 - 释放资源(只需要关闭
Socket
即可)
注意:
- 客户端和服务器端进行交互,必须使用
Socket
中getXXXX
提供的网络流,不要使用直接创建的流对象 - 当我们创建客户端对象
Socket
的时候,它就会去请求服务器和服务器经过3次握手,建立连接通路。这时,如果服务器没有启动,那么就会抛出ConnectException
异常;如果服务器已经启动,那么就可以进行交互。
ServerSocket类
ServerSocket
类:这个类实现了服务器套接字,该对象等待通过网络的请求。
构造方法
public ServerSocket(int port)
:使用该构造方法在创建ServerSocket
对象时,就可以将其绑定到一个指定的端口上
参数:int port
:端口号,服务器端ServerSocket
侦听该端口,一旦有客户端向该端口发送数据,服务器端就可以接收到
成员方法
服务器端只有一个,但是客户端可以有多个,所以服务器端必须明确一件事情,必须得知道是哪个客户端请求的服务器,所以首先使用accept
方法获取到请求的客户端对象Socket
public Socket accept()
:侦听并接受连接,返回一个新的Socket
对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。
所谓阻塞,就是既不继续向下运行,也不抛出异常终止程序,处在一个程序仍在运行但不向下运行的等待状态
服务器端实现步骤
- 创建服务器
ServerSocket
对象和系统要指定的端口 - 使用
ServerSocket
对象中的方法accept
,获取到请求的客户端Socket
对象 - 使用
Socket
对象中的方法getInputStream()
获取网络字节输入流InputStream
对象 - 使用网络字节输入流
InputStream
对象中的方法read
,读取客户端发送的数据 - 使用
Socket
对象中的方法getOutputStream()
获取网络字节输出流OutputStream
对象 - 使用网络字节输出流
OutputStream
对象中的方法write
,给客户端回写数据 - 释放资源(
ServerSocket
,Socket
都要关闭)
简单的TCP网络程序
TCP通信分析
- 服务端启动,创建
ServerSocket
对象,等待连接。 - 客户端启动,创建
Socket
对象,请求连接。 - 服务端接收连接,调用
accept
方法,并返回一个Socket
对象。 - 客户端
Socket
对象,获取OutputStream
,向服务端写出数据。 - 服务端
Scoket
对象,获取InputStream
,读取客户端发送的数据。
到此,客户端向服务端发送数据成功。
自此,服务端向客户端回写数据。
- 服务端
Socket
对象,获取OutputStream
,向客户端回写数据。 - 客户端
Scoket
对象,获取InputStream
,解析回写数据。 - 客户端释放资源,断开连接。
服务端释放资源(为了让服务器继续接收其他客户端发来的数据,通常不需要执行此步)
TCP通信实现
服务端实现:
1 | import java.io.IOException; |
客户端实现:
1 | import java.io.IOException; |
先运行服务器端,在运行客户端。服务器端的控制台打印:“你好服务器”,然后程序运行结束,客户端的控制台打印:“收到,谢谢” ,然后程序运行结束
综合案例
文件上传案例
客户端读取本地文件,把文件上传到服务器,服务器再把上传的文件保存到服务器的硬盘上
这里我们还是用之前IO流章节图片复制练习所使用的我老婆的图片,把我们本地计算机既当作客户端,又当作服务器端,要求读取C:\\我老婆.jpg
,然后上传给服务器,服务器将图片保存到D:\\upload
文件夹下
文件上传分析
- 客户端
new
本地字节输入流,从硬盘读取文件数据到程序中。 - 客户端
get
网络字节输出流,写出文件数据到服务端。 - 服务端
get
网络字节输入流,读取文件数据到服务端程序。 - 服务端
new
本地字节输出流,写出文件数据到服务器硬盘中。
至此已经完成:客户端读取本地文件,上传给服务器端,服务器端把文件写出到服务器文件
接下来是服务器向客户端写回“上传成功”信息
- 服务端
get
网络字节输出流,给客户端回写一个“上传成功” - 客户端
get
网络字节输入流,读取服务端回写的数据 - 释放资源
注意事项:
- 客户端和服务器端和本地硬盘进行读写,需要使用自己创建的字节流对象(本地流)
- 客户端和服务器之间进行读写,必须使用
Socket
中提供的字节流对象(网络流) - 文件上传的原理,就是文件的复制,明确数据源和数据的目的地
基本实现
服务端实现:
- 创建一个服务器
ServerSocket
对象,和系统要指定的端口号 - 使用
ServerSocket
对象中的方法accept
,获取到请求的客户端Socket
对象 - 使用
Socket
对象中的方法getInputStream
,获取到网络字节输入流InputStream
对象 - 判断
D:\\upload
文件夹是否存在,不存在则创建 - 创建一个本地字节输出流
FileOutputStream
对象,构造方法中绑定要输出的目的地 - 使用网络字节输入流
InputStream
对象中的方法read
,读取客户端上传的文件 - 使用本地字节输出流
FileOutputStream
对象中的方法write
,把读取到的文件保存到服务器硬盘上 - 使用
Socket
对象中的方法getOutputStream
,获取到网络字节输出流OutputStream
对象 - 使用网络字节输出流
OutputStream
对象的write
方法,给客户端回写"上传成功" - 释放资源(
FileOutputStream
,Socket
,为了继续侦听其他客户端发来的请求,一般不关闭ServerSocket
ServerSocket
)
1 | import java.io.*; |
客户端实现:
- 创建一个本地字节输入流
FileInputStream
对象,构造方法中绑定要读取的数据源 - 创建一个客户端
Socket
对象,构造方法中绑定服务器的IP地址和端口号 - 使用
Socket
中的方法getOutputStream
,获取网络字节输出流OutputStream
对象 - 使用本地字节输入流
FileInputStream
对象中的方法read
,读取本地文件 - 使用网络字节输出流
OutputStream
对象中的方法write
,把读取到的文件上传到服务器 - 使用
Socket
中的方法getInputStream
,获取网络字节输入流InputStream
对象 - 使用网络字节输入流
InputStream
对象中的方法read
,读取服务器端回写的对象 - 释放资源(
FileInputStream
,Socket
)
1 | import java.io.FileInputStream; |
注意:
这里我们要重点套路上面客户端实现中的第23行语句,如果不在这里加一个shutdownOutput()
,我们会发现,文件已经写入到服务器的文件夹中了,但是客户端的控制台没有打印“上传成功”,而且服务器和客户端的程序都没有停止运行。这说明程序运行到某一处之后处于了阻塞状态。是哪里呢?下面我们从头捋一捋:
- 首先运行服务器端,服务器创建
ServerSocket
对象,处于侦听状态 - 然后运行客户端,创建
FileInputStream
对象和Socket
对象中的方法getOutputStream
获取OutputStream
对象 - 客户端使用
FileInputStream
对象读取图片,使用OutputStream
对象将图片写出给服务器,采用的循环读取,终止条件是读取到结束符时终止,但是这样写入的字节是不包含终止符的 - 服务器端
accept
方法获取到请求的客户端Socket
对象,并使用getInputStream
获取InputStream
对象 - 服务器端使用
InputStream
对象读取客户端传来的字节,使用FileOutputStream
对象写出到服务器文件 - 同样是采用与第3步相同的循环读取,但是从客户端传来的字节是不含终止符的,所以读取的循环是一直不会终止的,所以在这里会处于阻塞状态,程序一直循环读取,一直循环写出,而后面的代码无法继续执行
因为程序一直在读取写入,所以程序结果会变成图片上传复制成功但程序没有终止,而且后续的服务器写回客户端的信息也没有打印,为了解决这个问题,我们需要使用shutdownOutput()
方法,这个方法可以关闭Socket
的OuputStream
流,并且在写出的字节最后加一个终止符。这样服务器端的循环读取就会在读取到结束符时停止读取。让程序可以继续向下进行。
文件上传优化分析
- 文件名称写死的问题
服务端,保存文件的名称如果写死,那么最终导致服务器硬盘,只会保留一个文件,建议使用系统时间优化,保证文件名称唯一
1 | String fileName = "perdant"+ System.currentTimeMillis() + new Random().nextInt(999999) + ".jpg"; // 一般格式是域名+时间+随机书+文件后缀 |
在网上下载下来的文件名一般都是很长一串数字,往往就是采用了这种命名方法,这样每次下载下来的相同的文件,名字却往往不一样
- 循环接收的问题
服务端,只保存一个文件就关闭了,之后的用户无法再上传,这是不符合实际的,使用循环改进,可以不断的接收不同用户的文件
1 | public class FileUploadServer { |
- 效率问题
服务端,在接收大文件时,可能耗费几秒钟的时间,此时不能接收其他用户上传,所以,使用多线程技术优化
1 | while(true){ |
我们把上面的所有优化(都是针对服务器端的)放入之前的代码中
1 | import java.io.*; |
模拟B/S服务器(扩展知识点)
浏览器端服务器端分析
在网络编程章节一开始,我们就讲过,除了C/S
结构,还有一种B/S
结构,就是浏览器与服务器连接的结构,和C/S
结构类似,只不过把客户端换成了浏览器,而之间交互的数据往往是html
文件。下面我来简单的捋一捋过程 然后写一个demo
,因为里面用到了html
文件,所以这里的代码你们可以自己先找一个html
文件,再来复现
- 浏览器里输入一个地址
- 服务器接收到浏览器发来的请求,根据请求的内容,解析出浏览器想要访问的文件的地址
- 服务器读取文件并向浏览器回写(一般不止一个
htm
l文件,我用一个web
文件夹储存了所有与网页有关的文件) - 浏览器中可以把这个
html
文件以网页的形式显示出来
我们之前的客户端就是现在的浏览器,相比C/S
,B/S只需要创建服务器端即可
1 | import java.io.*; |
上述记录于2019.11.26