Java笔记
一、入门概述
三个层次
- 基本语法
- 面向对象
- 高级应用
计算机包含硬件和软件两部分,软件提供看不见的指令,指令控制硬件,并且使得硬件完成特定的任务
我们写程序就是给机器、给硬件发指令。
创建或开发软件就叫程序设计
在个人计算机上,总线搭建在主板上,主板是一个连接计算机各个部分的线路板
CPU是计算机的大脑,它从内存中获取指令,然后执行这些指令。
CPU包括控制单元和算术/逻辑单元
- 控制单元用于控制和协调其它组件的动作
- 算术/逻辑单元用于完成数值运算和逻辑运算
每台计算机内部有一个内部时钟,该时钟以固定速度发送电子脉冲,时钟速度越快,在给定时间段内执行的指令就越多。速度的计量单位是赫兹,1hz相当于每秒1个脉冲。
超频降频都是指的这个hz,赫兹高了,自然每秒钟cpu执行的指令就多,电脑就快。
hz低了,那每秒钟执行的指令就少了,自然感觉就卡了(降频门事件)
内存(RAM)和磁盘的区别
内存中的信息在断电时会丢失,当计算机需要这些数据时,再移入内存,因此从内存中读取数据比从存储设备读取要快得多。
比特(bit)和字节(byte)
计算机就是一系列的电路开关,如果电路是开的,值是1,如果电路是关的,值是0
一个0或者1存储为1个bit,即1位,8位为一个字节,一个中文汉字一般为2个字节,2bytes,即16位。
计算机中的数据存储是二进制的方式,就是对应于电路的开关状态,bit是计算机的最小存储单位。
计算机中最基本的存储单元是字节,每个字节由8个bit构成。
计算机的存储能力是以字节和多字节来衡量的。
内存
内存(RAM)是由一个有序的字节序列组成,用于存储程序及程序需要的数据
一个程序和它的数据在被CPU执行前必须移到计算机的内存中。
CPU是执行各种计算机指令的,相当于是软件发出指令(所以软件会占用内存),CPU执行指令来调用硬件的运行。
CPU是从内存中来获取指令的
内存解决了一部分CPU运行过快,而硬盘数据存取太慢的问题。
但内存是带电存储的,一旦断电数据就会消失,而且容量有限,所以要长时间储存程序就需要存储到硬盘中
每个字节在内存(RAM)中都有一个唯一的地址
ROM和硬盘对应
软件层面最先和硬件打交道的就是操作系统
用户 -> 应用程序 -> 操作系统 -> 硬件
web开发,客户端(浏览器)访问浏览服务器提供的页面。
万维网(www)可以认为是一个由许多互相链接的超文本组成的系统。
每个有用的事物,称为一样资源,这些资源通过超文本传输协议(HTTP)传送给用户
HTTP就是客户端(浏览器)和服务器之间通信的协议!
万维网是由无数个网络站点和网页构成的集合,他们在一起构成了因特网最主要的部分。
万维网实际是多媒体的集合,是由超级链接连接而成的。我们通常通过网络浏览器上网观看的,就是万维网的内容。
JVM
是java virtual machine, java虚拟机,我们整个java程序想运行,都必须依赖java虚拟机。框架可以理解为对现有的一些基本组件的组合和封装,如果使用基本的组件去开发的话,效率很低,在实际场景中,如果什么组件都自己亲历亲为去写,效率低而且可能包括不全。
- 现在很多公司都基于微服务做开发
软件有系统软件和应用软件之分
系统软件就是操作系统
图形化界面(Graphical User Interface)
命令行方式(Command Line Interface)
安卓和IOS都是用的linux内核
二、Java语言概述
概述
虽然C和C++的开发效率不高,但是运行效率很高。开发得慢,运行得快
所以凡是和操作系统这种底层交互的,都用C和C++编写。
python比java开发效率更快,执行效率更差。相当于是编写过程中更复杂的语言,执行效率更快,因为写得复杂就相当于写得清楚,编译过程花的时间少,机器更容易理解,那么执行效率就更快。而Python这种编写很容易,阅读很容易,但是让机器理解就需要花费更多时间,也就是编译需要更多时间,也就是所谓的开发效率高,执行效率低。我们可以这么来理解。
python和java都可以做web开发, python可以用flask框架来实现。web开发要学会用postman或者swagger来进行调试。
python和JavaScript都可以看作轻型的语言,它们都是脚本语言,没有太严格的语法。
而java、C都是重型的语言,有严格的语法和丰富的类库
随着java技术在web方面的不断成熟,已经成为web应用程序的首选开发语言
java只支持类之间的单继承,但支持接口之间的多继承。
java语言的三个特点:
面向对象
- 两个基本概念:类、对象
- 三大特性:封装、继承、多态
健壮性
去掉了指针,有垃圾回收机制等
跨平台性
通过java语言写的应用程序在不同的系统平台上都可以运行
原理:只需要在需要运行Java程序的操作系统上,先安装一个Java虚拟机(JVM)即可,由JVM来负责java程序在该系统中的运行。
针对不同的操作系统,提供的JVM是不一样的
Java两种核心机制
- JVM---跨平台
- 垃圾回收机制
Java程序在JVM上跑,JVM是跑在操作系统上的。操作系统依赖于硬件、CPU去帮我们运算
垃圾回收机制
垃圾回收在C、C++语言中,是由程序员回收无用内存
Java提供一种系统级线程跟踪存储空间的分配情况,并在JVM空闲时,检查并释放那些可被释放的存储空间
垃圾回收在Java程序运行过程中自动进行,程序员无法精确控制和干预
但是Java程序仍然会出现内存泄漏和内存溢出问题
JDK
Java开发工具包,但是除了开发工具包以外,也包括了JRE,所以安装了JDK之后,就不用再单独安装JRE了,所以可能会认为JDK是Java运行环境,但是实际上JRE才是Java运行环境,只不过安装JDK的同时安装了JRE
JRE
java runtime environment
包括JVM和Java程序所需要的核心类库等所以如果想运行一个开发好的java程序,计算机中只需要安装JRE即可
运行java文件是先要把.java源文件编译成.class文件,.class文件是字节码文件,CPU是从内存中取出指令执行,hz数越高,在一定时间内执行的指令就越多。最底层的指令都是二进制的,因为CPU上有无数个电路开关,以0 1来控制。python是脚本语言,编写会很容易,但是计算机将其转换为字节码文件就会需要更多时间,也就是执行效率不高。
最终代码都是要转换成二进制序列才能够让机器执行,这是计算机的本质
字节码文件的名字是源文件.java中的类名
java路径是区分大小写的,但是windows不区分
编译的时候不写main函数没关系,但是运行的时候如果没有main函数就会报错,这其实是程序运行的入口
main()方法的格式是固定的
为什么要配置path环境变量,我们希望在任何路径下都能执行java开发工具
注释
注释和注解不一样,注释是comment、注解叫annotation
注释从字面意思来说就是对我们写的代码进行解释作用的
java规定了三种注释:
单行注释
// 单行注释
,单行注释不参与编译多行注释
/* 多行注释 main()方法是程序的入口 */ public static void main(String[] args { System.out.println("ermeihe fufu"); })
文档注释(java特有)
/** @author 指定java程序的作者 @version 指定源程序的版本 */
要注意文档注释的开头是两个
*
号,而多行注释的开头是1个*
号,但是结尾它们都是1个*
号文档注释的注释内容可以被JDK提供的工具javadoc所解析,生成一套以网页形式体现的该程序的说明文档
单行注释和多行注释的内容不参与编译
换句话说,编译之后会生成字节码文件。因为CPU是机器,执行的是二进制指令
编译之后生成的字节码文件不包含注释的内容
JavaAPI文档
类库这些就可以叫API
API---Application Programing Interface
应用程序编程接口API可以理解为接口,可以去调用API提供的功能、方法,但是不用关注其内部的程序是怎么执行。
API文档就是关于API怎么使用的说明
String类代表字符串类,Java程序中的所有字符串字面值都作为此类的实例实现
对一个Java程序进行总结
java程序编写-编译-运行的过程
- 编写:我们将编写的java程序保存在以“
.java
”结尾的源文件中 - 编译:我们使用
javac.exe
命令编译我们的.java
源文件。格式:javac 源文件名.java
- 运行:使用
java.exe
命令解释运行我们的字节码文件。格式:java 类名
- 编写:我们将编写的java程序保存在以“
一个源文件中可以声明多个class
并不是说一个.java源文件中只能有一个class,可以声明多个class
但是只能有一个类能够声明为
public
public
是面向对象关于封装性的一个关键字。那这个
public
关键字给哪个类呢?只能加在和源文件名相同的名称的类前面类名和文件名一样
程序的入口是main()方法,格式是固定的
输出语句
System.out.println();
先输出 后换行System.out.print();
编译的过程:编译以后,会生成一个或多个字节码文件,字节码文件的文件名与java源文件中的类名相同。也就是说编译生成的字节码文件的个数与源文件中类的个数相同
一个.java文件中可以有多个类,但是只有一个类能够声明为public关键字
前面加了public的类名一定要和源文件名称一样
三、Java基本语法
一、变量与运算符
关键字
关键字被Java语言赋予了特殊含义,用作专门用途的字符串(单词)
特点:关键字中所有字符都是小写
保留字
Java保留字是现有Java版本尚未使用,但以后版本可能会作为关键字使用,自己命名标识符时一定要避开这些保留字
goto
,const
标识符
Java对各种变量、方法、类等要素命名时使用的字符序列称为标识符
凡是自己可以起名字的地方都叫标识符
数字不可以开头
不可以使用关键字和保留字,但能包含关键字和保留字
Java中严格区分大小写,但是Windows不区分
长度无限制
标识符不能包含空格
Java中名称命名规范
包名:多单词组成时所有字母都小写
类名、接口名:多单词组成时候,所有单词的首字母大写
变量名、方法名:驼峰式命名
常量名:所有字母都大写,多单词时每个单词之间用下划线链接。
变量的概念:
内存中的一个存储区域
该区域的数据可以在同一类型范围内不停地变化
变量是程序中最基本的存储的那元,包含变量类型、变量名和存储的值
变量的作用:
用于在内存中保存数据,数据在内存中都是以二进制的形式保存的。每块存储区域都有唯一的地址
使用变量时注意:
- Java中每个变量必须先声明,后使用
- 使用变量名来访问这块区域的存储数据
- 变量只有在其作用域内才有效
- 同一个作用域内,不能定义重名的变量,但是变量在同一个作用域内可以多次赋值
Java对于每一种数据都定义了明确的具体数据类型(强类型语言),而JavaScript和python则没有,JavaScript用var来表示数据类型,python甚至不用定义。
Java定义数据类型,也是在内存中分配了不同大小的内存空间
Java中的数据类型分为两种
基本数据类型
数值型:
- 整数类型
byte
short
int
long
(存储空间从小到大) - 浮点类型
float
double
字符型:
char
布尔型:
boolean
- 整数类型
引用数据类型
类(
class
) < - (字符串在这里)接口
数组
[]
根据变量在类中声明的位置
- 成员变量
- 局部变量
字节才是计算机中基本存储单元
字符类型:
char
通常意义上,一个字符等于两个字节,一个字符要占两个字节的空间
定义
char
型变量,通常使用一对''
字符一定只能声明一个
我们怎么把我们写的代码中的这些字符对应成二进制,打开文件,计算机又是怎么将底层的二进制还原成我们所看到的字符,这就是字符集来指定的对应规则。
最简单的字符集就是ASCII码
一个byte对应于8bits,即一个字节对应于8位
在计算机内部,所有数据都使用二进制表示。每一个二进制有0和1两种状态,因此一个字节即8位可以组合出256种状态,从00000000-11111111
美国制定了一套字符编码,对英语字符和二进制位之间的关系做了统一规定。(A是65,a是97)
所以ASCII码足够只使用英语的国家使用
乱码:世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号,比如二进制01100001是97,被解释成a,在其他国家97就不解释成a,而是一个其他字符,这就导致了乱码的出现。
Unicode
一种编码,将世界上所有的符号都纳入其中,每一个符号都给予一个独一无二的编码,使用Unicode就没有乱码的问题utf-8
是在互联网上使用的最广的一种Unicode的实现方式utf-8
会根据字符的不同而变化字节长度,不会导致存储空间的浪费
二、自动类型转换和强制类型转换
前提:
7种数据类型之间的运算,布尔类型的变量没法做运算
‘a’ + 7
这也是可以运算的,'a'
在ASCII码和UTF-8都对应于97自动类型提升(转换)
byte
、short
、char
-->int
-->long
-->float
-->double
低级往高级提升当容量小的数据类型的变量与容量大的数据类型的变量做运算时,结果自动提升为容量大的数据类型。
说明:容量小和容量大指的是表示数的范围的大和小,比如 float容量要大于double容量
特别地:当byte、char、short三种类型的变量做运算时,结果为int类型。也包括同种类型的变量做运算,比如说两个byte类型的变量相加,结果为int类型。
换句话说:Java在做运算的时候,如果操作数均在int范围内,那么一律在int的空间内运算
强制类型转换
自动类型提升运算的逆运算。
自动类型提升是一个正常的过程,比如把字符类型的变量赋值给int类型的变量
假设有一个容量大的变量,想给它赋值到一个容量小的变量上。(这里的容量大小都是指的数的范围),比如把long型强转为int型
必须要使用强转符
()
可能出现精度损失,比如double型转换为int型,会出现精度损失,因为小数点后都没有了
会不会出现精度损失,要看具体数据类型的存储空间的大小和具体数值
double d1 = 12.3; int i1 = (int)d1; System.out.println(i1) // 12 这里叫截断操作,损失精度,小数点后都没有了。
整形常量默认类型为int型,浮点型常量默认类型为double型
String
String不是基本数据类型,属于引用数据类型,属于类
声明String类型变量时,使用一对
""
,要和char型变量区分开String可以和8种基本数据类型变量做运算。
String可以和布尔类型变量做运算,且运算只能是连接运算(用
+
拼一起)运算的结果仍然是String类型
System.out.println(* *); // 可以打印出* System.out.println('*' + '\t' + '*'); // 不可以,因为char类型变量相加是int类型 System.out.println('*' + "\t" + '*'); // 可以,这里的+号表示连接符 System.out.println('*' + '\t' + "*"); // 不可以,前面仍然是整形,第二个加号表示连接符 System.out.println('*' + ('\t' + "*")); // 可以,因为第二个加号是连接符,相当于char类型 // 和String类型相加,第一个加号仍然是连接符
三、关于进制
二进制的整数有如下三种形式
- 原码:直接将一个数值换成二进制数,最高位是符号位
- 负数的反码:是对原码按位取反,只是最高位作为符号位确定为1
- 负数的补码:其反码 + 1
计算机以二进制的补码形式来保存所有的整数
这里说的整数包括正数和负数,而正数的原码反码补码是相同的,所以也可以认为正数是以二进制的原码形式保存,但是这只是形式上的,实际上,正数和负数在计算机里都以补码的形式保存,所以计算得到补码之后,如果最高位是0,那么直接计算真数,如果最高位是1,说明这是用补码表示的负数,这个补码表示的负数的真数,一定要通过补码-1再取反,得到原码,再求得真数
正数的原码、反码、补码都是相同的
负数的补码是其反码+1
>>
右移,带符号右移,最高位补符号位
>>>
无符号右移,最高位补0
<<
左移,不论是正数还是负数,左移,最低位一定是补0.
一定要记住,所有的整数在计算机中用补码表示,所以我们在做题计算打草稿的时候,只要遇到负数,那么要用它的二进制计算,一定是通过补码进行计算,要得到真数,如果补码最高位是1,一定要转换为原码再得到真数,如果补码最高位是0,说明是正数,而正数的原码补码反码是相同的,所以可以直接计算真数!!
四、运算符
自增1、自减1不会改变原数据类型。
当
=
两侧数据类型不一致时,可以使用自动类型转换或使用强制类型转换原则进行处理支持连续赋值
short s1 = 10 s1 += 2 System.out.println(s1) // 这里能够正常输出,不会改变变量s1的原本数据类型。 // 但是写成s1 = s1 + 2 就不可以,因为,2默认为int类型,s1 + 2通过自动类型提升,提升为int类型,要继续赋值给s1,就是int类型赋值给short类型,高转低必须使用强转符
s1 += 2
s1++
这两种方式都不会改变s1
的原本数据类型逻辑运算符只能够适用于布尔类型的变量,而python则不是。
&
和&&
&
是逻辑与,符号两边必须都为true,结果才为true&&
是短路与,符号两边必须都为true,结果才为true。但是当符号左边已经不为true了,符号右边就不再计算了,因为结果已经为false。这种情况下,逻辑与仍然会执行右边。相同点:运算结果相同
当符号左边是true时,二者都会执行符号右边的运算
不同点:当符号左边是false时,&继续执行右边的运算,&&则不会执行右边的运算
开发中,优先使用短路与和短路或
位运算符
- 位运算符操作的都是整型的数据
<<
在一定范围内,每向左移一位,相当于 * 2>>
在一定范围内,每向右移一位,相当于 / 2
三元运算符可以嵌套,但是返回的类型要一致,要能够用一个变量去接收。
凡是可以使用三元运算符的地方,都可以改写为
if-else
但是
if-else
不是一定能改写成三元运算符if-else
是一个大的流程控制语句,能够写得很复杂如果程序既可以使用
if-else
,又可以使用三元运算符,那我们优先选择三元运算符,原因:简洁、执行效率高
五、程序流程控制
结构化程序设计中规定的三种基本流程结构:
- 顺序结构
- 分支结构
- 循环结构
顺序结构:
程序从上到下逐行地执行,中间没有任何判断和跳转
分支结构: 根据条件,选择性地执行某段代码
有
if-else
和switch-case
两种分支语句循环结构:
根据循环条件,重复性地执行某段代码
有
while
do...while
for
三种循环语句现在有
for each
循环条件表达式返回的都是
boolean
类型的。如何从键盘获取不同类型的变量:需要使用Scanner类
具体实现步骤:
导包:
import java.util.Scanner;
Scanner的实例化
其实就是实例scanner的对象Scanner scanner = new Scanner(System.in);
调用Scanner类的相关方法,来获取指定类型的变量
switch-case
结构根据
switch
表达式中的值,依次匹配各个case中的常量,一旦匹配成功,则进入相应的case中,调用其执行语句,当调用完执行语句之后,则仍然继续向下执行其他case结构中的执行语句, 直到遇到
break
关键字或此switch-case结构末尾结束为止要想实现多选一的情况,在每个case结构中都要加上
break
default中可以不加break
break
关键字可以使用在switch-case结构中,表示一旦执行到关键字,就跳出switch-case结构
switch
结构中的表达式只能是如下六种数据类型之一:byte short char int 枚举类型 String类型
case
之后只能声明常量、不能声明范围break关键字在switch-case结构中是可选的,根据实际情况看要不要加
default
相当与if-else结构中的elsedefault
结构是可选的,不是必须有。而且位置是灵活的
如果switch-case结构中的多个case的执行语句相同,则可以考虑进行合并
说明:
- 凡是可以使用switch-case的结构,都可以转换为if-else,反之不成立,因为switch的要求更高一些。
- 如果两者都可以,同时switch中表达式的取值情况不太多,优先选择switch-case,原因:switch-case的执行效率稍微高些
六、循环结构
在某些条件满足的情况下,反复执行特定代码的功能
循环语句分类:
for
while
do while
循环语句的四个组成部分
- 初始化部分
- 循环条件部分(
boolean
类型) - 循环体部分
- 迭代部分
break
一旦在循环中执行到break,就跳出循环do-while
循环,先执行了一次循环体!不在循环条件部分限制次数的结构:
for(;;)
while(true)
结束循环有几种方式:
- 循环条件部分返回false
- 在循环体中,执行break
break
在循环结构中的作用是结束当前循环,默认跳出包裹此关键字最近的一层循环,还可以结束指定标识的一层循环结构continue
的作用是结束当次循环,也可以结束指定标识的一层循环结构的当次循环break
还可以用在switch-case结构中break
continue
的后面都不能声明执行语句return
并非专门用于结束循环的,它的功能是结束一个方法,与break和continue不同的是,return直接结束整个方法,不管这个return处于多少层循环之内
Utility
工具类将不同的功能封装为方法,就是可以直接通过调用方法使用它的功能,而无需考虑具体的功能实现细节。
四、数组
一、概述
数组是多个相同类型的数据按照一定顺序排列的集合。和JavaScript、python不一样,JavaScript、python他们的列表里都是可以存放不同类型的数据,还可以存放对象。Java也有列表list。
数组本身是引用数据类型
数组的元素既可以是基本数据类型,也可以是引用数据类型
数组的长度一旦确定就不能更改。
创建数组对象会在内存中开辟一整块连续的空间,而数组名中引用的是这块连续空间的首地址。
数组的分类:
- 按照维数:一维数组、二维数组...
- 按照数组元素的类型:基本数据类型元素的数组、引用数据类型元素的数组
二、一维数组的使用
一维数组的声明和初始化
int[] ids; // 声明 // 1.1 静态初始化 ids = new int[]{1001, 1002, 1003, 1004}; // 1.2 动态初始化 String[] names = new String[5];
这里可以看出为什么数组和字符串是引用数据类型,数组初始化的时候需要new,字符串实际是有一个String类,实例化对象的时候需要
new
关键字静态初始化指定了元素----数组初始化和数组元素的赋值操作同时进行
动态初始化没有指定元素是什么----数组的初始化和数组元素的赋值操作分开进行
不管是动态初始化还是静态初始化,一旦初始化以后,数组的长度就确定了。在静态初始化中,数组的长度是数组元素的个数,如果长度确定不了,内存中不知道要开辟多少长度,数组在内存中就体现为一片连续的内存空间。
如何调用数组的指定位置的元素
通过索引的方式进行调用,索引是从0开始
程序编译只是说将程序文件生成字节码文件,供CPU执行,CPU是从内存中取出指令执行,指令在计算机中最底层体现为二进制码。
运行的时候才是真正把程序加载到内存中,比如说声明了长度为5的数组,在赋值的时候,超出了长度,编译是能通过的,因为编译是生成字节码文件,运行的时候才会报错,因为运行才会加载到内存,内存中开辟的连续的存储空间为数组的长度,如果超过就会报错。
如何获取数组的长度
如何遍历数组
数组元素的默认初始化值
没有给数组显式赋值,数组有默认值
- 整型数组,默认值是
0
- 浮点型数组,默认值是
0.0
- char型数组,默认值是
0
或\u0000
,而非'0'
- boolean型数组,默认值是false
- 引用类型数组,默认值是
null
- 整型数组,默认值是
数组的内存解析
new
是典型的实例化对象的方式栈:stack,线性表
堆:heap
方法区
内存结构的简图
栈主要存放的是局部变量,局部变量是在方法中定义的变量
堆中存放的是new出来的结构,典型的就是对象和数组
int[] arr = new int[]{1, 2, 3}; String arr1 = new String[4]; arr1[1] = "刘德华"; arr1[2] = "张学友"; arr1 = new String[3]
用到的是栈结构和堆结构
第一行:局部变量应该放在栈中。右边结构放在堆中,为连续的一片内存区域,连续空间的首地址值用十六进制来表示,这个地址值赋值给栈空间的arr,栈空间的arr通过这个地址值就可以找到堆空间的数组了。
只要见到
new
了,堆空间中就重新开辟内存空间
三、二维数组
对于二维数组的理解,我们可以看成是一维数组array1又作为另一个一维数组array2的元素而存在,其实,从数组底层的运行机制来看,其实没有多维数组
数组是引用数据类型,数组的元素可以是引用数据类型也可以是基本数据类型,所以数组的元素也可以是数组
如果一维数组的元素还是一维数组,整体上来看就是二维的。
空指针异常:对象引用没有指向实例化对象
二维数组的长度
指的最外层一维数组的长度,无所谓里面每一个数组元素是多少长度
二维数组分为外层数组的元素、内层数组的元素
int[] arr = new int[4][3]; // 外层元素:arr[0],arr[1]等 // 内层元素:arr[0][0]等
初始化一维数组后,输出变量,就是地址,因为一维数组的变量被声明在栈中,就是地址,这个地址值是堆中的开辟的连续内存空间的首地址值。
这个栈中的变量或者叫对象引用就是堆中对应的实例化对象的地址,这个概念要到面向对象才能说清。
数组是属于引用数据类型,默认值是null
针对于初始化方式一
int[][] arr = new int[4][3]
外层元素的初始化值:地址值(第二层数组所开辟的内存空间的首地址值)
内层元素的初始化值为:与一维数组初始化情况相同
针对初始化方式二
int[][] arr = new int[4][]
外层元素的初始化值:null
内层元素的初始化值:不能调用,否则报错
因为没有给第二层数组做初始化,没有从第一层元素指向第二层数组的首地址的地址值,引用类型,默认值为null
内层元素不能用,因为根本没有对象引用指向某个内层数组,还没有分配,一用就会报空指针异常
数组中涉及的常见算法
数组元素的赋值(杨辉三角、回形数等)
求数值型数组中元素的最大值、最小值、平均数、总和等
数组的复制、反转、查找(线性查找、二分法查找)
int[] array1, array2; array1 = new int[]{2,3,3,3}; array2 = array1;
并没有在内存的堆空间中另外开辟一片内存空间,而是使array2指向了array1所指向的内存空间中的数组实体。array1其实是地址,把地址给了array2,他们就指向了同样的地址,并没有新开辟空间。
这种问题在实例化对象中还会遇到
还有一个角度,那就是new关键字出现了,才代表在内存的堆空间中开辟了新的空间
这里是array2和array1地址值相同,都指向了堆空间的唯一一个数组实体。相当于在windows中发送快捷方式
数组赋值赋的是地址,而复制则是指要新开辟一块连续的内存空间
数组元素的排序算法
线性查找
二分法查找
前提:所要查找的数组必须有序
while (head <= end) { int middle = (head + end) / 2 if (dest == arr[middle]) { System.out.println("找到"); } else if (dest < arr[middle]) { end = middle - 1; } else { head = middle + 1; } }
通常来说,排序的目的是为了快速查找,比如为了用二分法,那么就得先排序
衡量排序算法的优劣:
- 时间复杂度:分析关键字的比较次数和记录的移动的次数
- 空间复杂度:分析排序算法中需要多少辅助内存
- 稳定性:若两个记录A和B的关键字值相等,但是排序后A和B的先后顺序保持不变,则称这种排序算法是稳定的。反之就是不稳定的。
高效率和低存储!
排序算法分类:
- 内部排序:整个排序过程,不需要借助外部存储器(如磁盘等),所有排序操作都在内存中完成
- 外部排序:参与排序的数据非常多,数据量非常大,计算机无法把整个排序过程放在内存中完成,必须借助于外部存储器(如磁盘),外部排序常见的是多路归并排序。可以认为外部排序是由多次内部排序组成。
十大排序算法
选择排序
- 直接选择排序
- 堆排序
交换排序
- 冒泡排序
- 快速排序
插入排序
- 直接插入排序
- 折半插入排序
- shell排序(希尔排序)
归并排序
桶式排序
基数排序
冒泡排序:
通过对待排序序列从前向后,依次比较相邻元素的排序码,若发现逆序则交换,使排序码较大的元素逐渐从前移向后部。
每一趟进行的过程:从第一个元素开始,比较两个相邻的元素,若相邻元素的相对位置不正确,则进行交换
结束条件:在任何一趟进行过程中,未出现交换
快速排序是迄今为止所有内排序中最快的一种。
方法内部调用自己这个方法,叫做递归调用
一旦程序出现异常,就终止执行,前提是异常未处理
五、面向对象
一、Java类及类的成员
java由两大要素,类和对象
三大特性:面向对象性、健壮性、跨平台性
属性、方法、构造器、代码块、内部类
后面两个在实际开发中用得少一些
面向对象三大特征
封装性、继承性、多态性、(抽象性)
主要关注在代码上怎么去体现的
面向过程和面向对象
面向过程强调的是功能行为,以函数为最小单位,考虑怎么做。
面向对象,将**功能封装进对象,强调具备了功能的对象**,以类/对象为最小单位,考虑谁来做。功能在对象里
这二者都是一种思想。
面向对象的扩展性强,如果要添加功能(函数、方法),在类里面添加。
相当于公司都有自己的部门,每个部门都有自己的职责和规章制度,相当里类里面的方法。
人把大象装进冰箱
面向过程:强调功能行为本身
面向对象:强调具有功能的对象,以类/对象为最小单位,关注类、对象
面向对象分析方法分析问题的思路和步骤
- 根据问题需要,选择问题所针对的现实世界中的实体
- 从实体中寻找解决问题所需要的属性和功能,这些属性和功能形成了概念世界中的类
- 把抽象的实体(类)用计算机语言进行描述,形成计算机世界中类的定义
- 将抽象的类实例化成对象,对象是计算机世界中解决问题的最终工具
类(class)和对象(object)是面向对象的核心概念
类是对一类事物的描述,是抽象的、概念上的定义
对象是实际存在的该类事物的每个个体,因此也称为实例(instance)。对象是具体化的实例!
万事万物皆对象,对象是实例。所以new关键字的作用也叫实例化对象(将抽象类实例化)
类如果是抽象概念的人,对象就是实实在在的某个人
面向对象程序设计的重点就是类的设计
类的设计其实就是指的类的成员的设计
抽象的类具有什么功能,具体的实例(对象)才能实现什么功能
类的成员
属性、方法是类当中最重要的两个结构(成员)
属性就是成员变量,行为就是成员方法。
java代码世界是由多个不同功能的类构成的。
属性:
就是成员变量,就是一个意思。英文叫field
属性 = 成员变量 = field = 域、字段
方法 = 成员方法 = 行为 = 函数 = method
如何使用Java类
创建类的对象 = 类的实例化
在类中定义属性也就是成员变量,有默认初始化值!
如果创建了一个类的多个对象,则每个对象都独立地拥有一套类的属性(非static的)。
意味着,如果我们修改一个对象的属性a,则不影响另一个对象的属性a的值
Person p1 = new Person(); Person p3 = p1;
这里的p1指的是对象引用,右边的new Person()是对象实例,
=
号的意思是说将p1这个对象引用指向了内存的堆空间中的对象实例,直接打印p1是一个地址值,这个地址值指的就是堆空间中实例化对象的地址值。p1这个对象引用保存的对象地址值赋给p3,导致p1和p3指向了堆空间中的同一个对象。
对象引用(reference类型)
它不等同于对象本身,是对象在堆内存放的首地址。方法执行完,自动释放,对象引用和局部变量存储于虚拟机栈中。
局部变量
main方法是一个方法,方法中的变量,都是局部变量
局部变量存储于栈空间中,而成员变量,就是在类下面且方法外部定义的变量,叫成员变量,存储于堆空间中!
属性(成员变量) vs 局部变量
相同点:
- 1.1定义变量的格式:数据类型 变量名 = 变量值(成员变量可以不用手动初始化,有默认初始化值)
- 1.2先声明,后使用
- 1.3变量都有其对应的作用域
不同点:
2.1.在类中声明的位置的不同
属性:直接定义在类的一对{}内
局部变量:声明在方法内、方法形参、代码块内、构造器形参、构造器内部的变量
public void talk(String language) { } // language是方法形参,是局部变量 public void eat(){ String food = "烙饼"; // food 是方法内变量,局部变量 } // 以上是两个局部变量典型位置
2.1 关于权限修饰符的不同
属性(成员变量):可以在声明属性时,指明其权限,使用权限修饰符
常用的权限修饰符:private、public、缺省、protected
加了权限,能决定在什么地方能调用这个类的属性,用于实现类的封装。
局部变量:不可以使用权限修饰符
2.3 关于默认初始化值
属性:有默认初始化值的,根据其类型决定默认初始化值是多少。
整型(byte、short、int、long):0
浮点型(float、double):0.0
字符型(char):0或'\u0000'
布尔型(boolean):false
引用数据类型(类、数组、接口):null
局部变量:没有默认初始化值,意味着我们在调用局部变量之前一定要显式赋值。
特别地:形参在调用的时候,赋值即可。对于方法内部定义的局部变量,一定要显示赋值
在内存中的位置
属性:加载到堆空间中(非static)。static的属性加载到方法区
局部变量:加载到栈空间
类中方法的声明和使用:
方法:是来刻画类应该具有的功能,比如Math类:sqrt()、random()
Scanner类:nextXxx()方法
方法的声明:权限修饰符 返回值类型 方法名(形参列表)
形参可以有也可以没有
权限修饰符 返回值类型 方法名(形参列表) { 方法体 }
权限修饰符:方法被调用的时候的权限大小(可不可以调用此方法)
Java规定的4种权限修饰符:private、public、缺省、protected
返回值类型:有返回值 vs 没有返回值
- 如果方法有返回值,则必须在方法声明时指定返回值的类型,需要方法中需要使用return关键字返回指定类型的变量或常量
- 如果方法没有返回值,方法生命时,使用void来表示,通常没有返回值的方法中就不需要使用return,但是如果使用的话,只能
return;
表示此方法结束
方法的使用中,可以调用当前类的属性或方法
特殊的,方法A中又调用了方法A,叫递归方法
递归也需要有终止。
方法里面不能定义别的方法
静态方法和静态变量可以不实例化对象而直接通过类名.属性名/方法名的形式调用
数组里面也可以装对象---对象数组
引用类型变量里存的不是null就是地址,如果new过,并通过
=
号使对象引用指向了new的部分,那么存的就是地址,否则就是null写冒泡排序,需要换序,交换的使数组中的元素,如果数组中存的是对象,那么交换的也是对象!!!
想在main方法里调写在同一个类的方法,因为main方法是静态的,静态方法不能调用非静态的方法和属性,所以第一种是写在类里main方法外的其它方法要加上static关键字修饰。
第二种是,在main方法里实例化这个类,通过类的实例化对象去调。main方法也是属于这个类的,仍然可以在方法内部实例化这个类。
public class StudentTest { public static void main(String[] args) { StudentTest studentTest = new StudentTest(); } }
不要认为类的实例化只有在这个类的外面才可以做,在这个类的里面也可以做!!
类里的某个方法可以调用同类的其他方法
具体怎么写,要看这个方法是否是静态的,如果是,那么所调用的其他方法也要是静态的,否则就要实例化对象!
JVM内存结构
源代码编译以后生成的是字解码文件(.class),字节码文件仍然是存放在硬盘中的,只有运行的时候才会加载到内存中。
然后解释运行,运行的时候才把字节码文件加载到内存中,然后分配空间
编译完源程序后,生成一个或多个字节码文件
我们使用JVM中的类的加载器和解释器对字节码文件进行解释运行,意味着,需要将字节码文件对应的类加载到内存中,涉及到内存解析。
虚拟机栈,即为平时提到的栈结构,我们将局部变量存储在栈结构中
堆,将new出来的结构加载在堆空间中,比如数组、对象。
补充:对象的属性加载在堆空间中,这是成员变量,要和局部变量区分开(目前没有涉及到static)
方法区:类的加载信息,常量池,静态域
万事万物皆对象
理解这句话:
在Java语言范畴中,我们都将功能、结构等封装到类中,通过类的实例化,来调用具体的功能结构。Java语言项目是有多个具有不同功能的类组成的。
Scanner, String
File
URL
涉及到Java语言与前端HTML,后端的数据库交互时,前后端的结构在Java层面交互时,都体现为类、对象
引用类型的变量只可能存储两类值:null或地址值(含变量的类型)
在同一个类中,A方法内部想调用B方法,如果A方法是static,那么
- B方法也是static,直接调
- 实例化这个类,在通过实例化之后的对象来调。(一个类要实例化,并不是说只能在这个类外部才能实例化,在这个类内部也可以实例化)
如果A方法不是static修饰,那么就可以直接调用B方法。
方法的重载
定义:在同一个类中,允许存在一个以上的同名方法,只要他们的参数个数或参数类型不同即可。
两同一不同:
同一个类、相同方法名
不同参数列表:参数个数不同,参数类型不同
重载的特点:
与返回值类型无关,只看参数列表,且参数列表必须不同(参数个数或参数类型)。
调用时,根据方法参数列表的不同来区分调用哪个方法。
跟方法的权限修饰符,返回值类型,形参变量名,方法体都没有关系
可变个数形参的方法
这是jdk5.0新增的内容,允许直接定义能和多个实参(0个、1个或多个)相匹配的形参。从而,可以用一种更简单的方式,来传递个数可变的实参。
public void show(String ... strs) { }
可变个数形参的格式
数据类型 ... 变量名
当调用可变个数形参的方法时,传入实参的个数可以是0个、1个、2个或多个。。。。
可变个数形参的方法与本类中方法名相同,形参类型也相同的数组的方法不构成重载,换句话说,二者不能共存
可变个数形参在方法的形参中,必须声明在末尾且最多只能声明一个可变形参
方法参数的值传递机制
方法,必须由其所在类或对象调用才有意义,若方法含有参数:
形参:方法声明时的参数
实参:方法调用时实际传给形参的参数值
Java里的实参值如何传入方法呢?
Java里方法的参数传递方式只有一种:值传递,即将实际参数值的副本(复制品)传入方法内,而参数本身不受影响
形参是基本数据类型,将实参基本数据类型变量的“数据值”传给形参
形参是引用数据类型,将实参引用数据类型变量的“地址值”传给形参
换句话说
实参赋给形参这个过程,如果是基本数据类型,赋的是数据值
如果是引用数据类型,赋的是地址。
关于变量的赋值
如果变量是基本数据类型,此时赋值的是变量所保存的数据值
如果变量是引用数据类型,此时赋值的是变量所保存的数据的地址值
==
号比较如果是基本数据类型,比的是变量所保存的数据值,具体存的这个值
如果是引用数据类型,比的是地址值。
因为引用数据类型变量,存的不是地址值就是null
public static void main(String[] args) { int[] arr = new int[]{1, 2, 3}; System.out.println(arr); // 地址值 char[] arr1 = new char[]{'a', 'b', 'c'}; System.out.println(arr1); //abc }
这是因为下面的println方法和上面的println方法,是重载的关系,也就是说Java里有专门打印字符串数组的方法,通过方法重载来实现。
String str = new String("abc"); System.out.println(str);//abc
String类型变量,也是属于引用类型变量,是new出一个类的实例。类、数组、接口是引用类型。
引用类型变量,在内存中的值不是null就是地址值。左边的str是对象引用,这个对象引用,指向了内存中堆空间中的String实例。那么这个对象引用str就不是null,而应该是一个地址值,但是打印出来是abc,这同样是因为println方法和最上面的println方法是方法重载的关系。
public class ValueTransferTest { public static void main(String[] args) { String s1 = "hello"; ValueTransferTest test = new ValueTransferTest(); test.change(s1); System.out.println(s1); } public void change(String s) { s = "hi"; } }
按照之前的理解,会认为打印出来的s1变成了hi,因为对于引用数据类型变量,值传递传递的是地址,新声明的形参也有了地址值,指向堆空间中new出来的区域,对这个s进行了操作,那么堆空间中的值也相应改了,会这样认为,但是实际上s1这个对象引用指向的不是堆空间,而是常量池
String作为引用类型,s1存的不是null就是地址值,由于有赋值符号,那么说明s1这个对象引用指向了内存中的某个地址,s1一定存的是地址值,而且它比较特别,特别在在内存中的存储,String类型的值存储在内存中的常量池,常量池中字符串的字符序列是不可变的。
递归方法
递归方法:一个方法体内调用它自身
方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制。
递归一定要向已知方向递归,否则这种递归就变成了无穷递归,类似于死循环
递归一定要设置返回值!类似于很多数学题的f(0)=0这种初始值。
if (n == 1) { return 1; } else { return n + getSum(n - 1); }
这就是朝着n==1递归,朝着已知方向递归,最终会return1,朝着这个方向递归。最后才会有结果
二、面向对象的三大特征
一、封装性
高内聚:类的内部数据操作细节自己完成,不允许外部干涉
低耦合:仅对外暴露少量的方法用于使用
隐藏对象内部的复杂性,只对外公开简单的接口,便于外界调用,从而提高系统的可扩展性、可维护性。
通俗地说,把该隐藏的隐藏起来,把该暴露的暴露出来,这就是封装性的设计思想。
成员变量前面加上private访问修饰符之后就不允许直接实例化对象,然后通过对象名.属性名 = 值 的方式赋值了。
private修饰符就说明属性是私有的,不能调,就说明属性没有对外暴露。要想调用,只能通过set方法赋值,这就是set和get方法。
问题的引入:
当我们创建一个类的对象以后,我们可以通过对象.属性的方式,对对象的属性进行调用,这里,赋值操作要受到数据类型和存储范围的制约。但是除此之外,没有其他制约条件。
但是,在实际问题中,我们往往需要给属性赋值加入额外的限制条件,这个条件就不能在属性声明时体现,就需要专门定义一个set方法来进行限制条件的添加。
同时,我们需要避免用户再使用对象.属性的方式来对属性进行调用,则需要将属性声明为私有的。属性是成员变量。
此时,针对于属性就体现了封装性。
封装性的体现:
将类的属性私有化,同时,提供公共的方法来获取和设置属性的值。(get和set)
拓展:封装性的体现。
如上
不对外暴露的私有的方法,这些方法供类下面的其他方法进行内部调用。
单例模式也是
封装性的体现,需要权限修饰符来配合。
Java规定了4种权限。(从小到大排列)
private、缺省(什么也不写,也是一种权限)、protected、public
看方法、属性、能不能调,就看权限的大小。
对于class的权限修饰只可以用public和缺省
权限可以用来修饰类和类的内部结构:属性、方法、构造器、内部类
总结封装性:
Java提供了4种权限修饰符来修饰类及类的内部结构,体现类及类的内部结构在被调用的时候的可见性大小
通常习惯一个源文件里写一个类
类的成员之三:构造器(或构造方法)--constructor
任何一个类都有构造器,即使我们没有定义,默认也有。
构造器的作用:
创建对象
Person p = new Person()
右边这个
Person()
就是构造方法。我们没有定义构造器,发现也有构造器。
初始化对象的属性或者说信息!
关于构造器的说明:
如果没有显示地定义类的构造器的话,则系统默认提供一个空参的构造器。(默认构造器的权限和类的权限是相同的)
定义构造器的格式:权限修饰符 类名(形参列表){}
new对象的时候用的就是构造器。
严格来讲,构造器不要理解为方法,构造器的作用是造对象。
构造器是constructor,方法是method
在一个类中构造器可以定义多个,这也叫做重载!
构造器可以使得在造对象的时候,就初始化对象了。
如果一个构造器都没有写,那么java就提供一个默认的空参的构造器,如果显示地定义了类的构造器,那么空参的构造器就不提供了。所以在定义的时候,也要把空参的构造器显示写出来
总结:属性赋值的先后顺序
- 默认初始化
- 显示初始化
- 构造器
- 通过对象.set方法 或 对象.属性(如果有权限)
以上操作的先后顺序
1 - 2 - 3- 4
JavaBean
所谓JavaBean,是指符合如下标准的Java类:
- 类是公共的
- 有一个无参的公共的构造器
- 有属性,且有对应的get、set方法
二、继承
继承性的好处:
- 减少了代码的冗余
- 便于功能的扩展
- 为之后的多态性的使用,提供前提。
继承性的格式:
class A extends B {}
A:子类、派生类、subclass
B:父类、超类、superclass
体现:一旦子类A继承了父类B之后,子类A就从父类B中获取到了声明的所有属性和所有方法
特别地,父类的私有的属性和方法也被子类继承到了,只是由于封装性的影响,不能直接调用。
封装性解决的是结构可见性的问题,继承性解决的是子类能不能拿到父类结构的问题,只要继承,是能拿到的,只是由于封装性的影响,不能直接调用。
子类继承父类之后,还可以声明自己特有的属性和方法,实现功能的拓展。子类和父类的关系一定不要认为等同于父集和子集的关系。
Java中关于继承性的规定:
- 一个父类可以有多个子类。一个类可以被多个子类继承
- Java中类的单继承性:不允许多重继承。一个类只能有一个父类。但是可以有多个接口(C++是支持多重继承的。)
- 子类父类是相对的概念,类是可以多层继承的,但是不可以多重继承!
- 子类直接继承的父类叫直接父类,间接继承的父类叫间接父类
- 子类继承父类之后,就获取了直接父类以及所有间接父类中声明的属性和方法。
如果我们没有显示地声明一个类的父类,则此类继承于
java.lang.Object
类所有的类都直接或间接地继承于
java.lang.Object
类。意味着所有的类都具有Object类中声明的功能。
方法的重写:override/overwrite
重载:overload
属性是成员变量,存储于堆空间中,有默认初始值!
重写:
在子类中可以根据需要对从父类中继承来的方法进行改造。在程序执行的时候,子类的方法将覆盖父类的方法。换句话说:
子类继承父类以后,可以对父类中同名同参数的方法,进行覆盖操作。
重写应用:
重写以后,当创建子类对象以后,通过子类对象去调用子父类中同名同参数的方法时,实际执行的是子类重写父类的方法。父类对象当然仍然调用父类自己的方法。
重写的规定
方法的声明:权限修饰符 返回值类型 方法名(形参列表) throws 异常的类型 {
方法体;
}
约定俗称:子类中的叫重写的方法,父类中的叫被重写的方法。
子类重写的方法的方法名和形参列表与父类被重写的方法的方法名和形参列表相同。
子类重写的方法的权限修饰符不小于父类的被重写的方法的权限修饰符(子类权限修饰符不能比父类权限修饰符小,才能覆盖得住!可以这么理解)
子类不能重写父类中的权限修饰符为private的方法。
返回值类型
- 如果父类中的方法是void,那么子类中重写的方法也只能是void
- 父类被重写的方法的返回值类型是A类型,则子类重写的方法的类型可以是A类或A类的子类。(针对于返回值类型是引用数据类型)
- 方法的返回值类型除了是引用数据类型,还可以是基本数据类型,如果父类的被重写的方法的返回值类型是基本数据类型,那么子类的重写的方法的返回值类型也必须是相同的基本数据类型
子类重写的方法抛出的异常类型不大于父类被重写的方法所抛出的异常类型。(如果被重写的方法抛出了异常)
子类和父类中的同名同参数的方法要么都声明为非static的,要么都声明为static的,只有当声明为非static的时候我们才考虑去重写。静态的就不叫重写了。父类中静态声明的方法一定是不可以被重写的,是因为静态的方法不能够被覆盖,是随着类的加载而加载的。(如果父类中静态的方法,在子类中声明同名同参数的静态方法不会报错,但是这不叫做重写!虽然不报错!)
面试题:区分方法的重载与重写。
重载就是在同一个类中,同名的方法, 但是参数列表不同(参数类型或个数不同)(两同一不同)
重写是发生在子类中,将继承的父类的同名同参数方法进行一个重写,进行了覆盖操作。
super
有这么一种情况:父类中定义了一个方法,子类中把这个方法重写,现在想在子类中再去调用父类的这个方法而不是子类重写之后的方法
super关键字的使用
- super理解为父类的,就像this理解为当前对象的。
- super可以用来调用属性、方法、构造器
- 我们可以在子类的方法或构造器中,通过使用
super.属性``super.方法
的方式,显示地调用父类中声明的属性或方法, 但是,通常情况下,我们习惯省略super.
(看起来好像是没定义就直接使用一样,但是实际上是使用继承的父类的属性和方法) - 当子类和父类当中,定义了叫同名的属性的时候,要想再子类当中调用父类中声明的属性,则必须显示的使用
super.属性
的方式,表明调用的是父类中声明的属性。 - 当子类重写了父类的方法以后,我们想在子类的方法中调用父类中被重写的方法时,必须显式地使用super.方法的方式,表明调用的是父类的方法。
super调用构造器
- 可以在子类构造器中显式地使用super(形参列表)的方式,调用父类中声明的指定的构造器。(前提是父类中有这个形式的构造器)
- super(形参列表)的使用,必须声明在子类构造器的首行。
- 在一个类的构造器中,this(形参列表)和super(形参列表)只能二选一,不能同时出现
- 这个时候理解this修饰构造器就好理解了,this指的就是当前对象!而super指的是父类!
- 如果自己在构造器首行既没有写this,也没有写super,默认是super(),默认调的是父类的无参构造器!无参构造器也是默认的。
- 在类的多个构造器中,至少有一个类的构造器中使用了super(形参列表)的方式表示调用父类中的构造器。
子类对象实例化过程:
A类有几个父类B、C、D
现在创建一个A的对象,new完的这个对象,new A()在堆空间中这个结构来看,都会加载父类的属性和方法,这个就叫做继承性。继承的父类的属性和方法不用在子类中显式地写出来。思维不要定势,不要认为一定要定义了才有,既然是继承,那么子类就有父类所有属性和方法了。就像声明一个类,什么都不定义,这个类具有Object类的一些方法
从结果上来看:但子类继承父类以后,子类继承了父类声明的属性和方法,当new(创建)了子类对象,子类对象在堆空间中加载了父类的这些结构,就可以直接调了!
从过程上来看,为什么会加载父类的结构,因为我们通过子类构造器创建子类对象时,我们一定会直接或间接地调用其父类构造器,进而调用父类的父类的构造器。。。。直到调用了java.lang.Object中空参的构造器。正因为加载了所有的父类的结构,所以才看到内存中有父类的结构,子类对象才可以考虑进行调用。
三、多态性
理解多态性:可以理解为一个事物的多种形态。
何为多态性:父类的引用指向子类的对象或子类的对象赋给父类的引用
右边的对象体现为多种形态
当调用子父类同名同参数方法时,实际调用的是子类重写父类的方法。----虚拟方法调用
Person p2 = new Man(); p2.eat(); p2.walk();
对象引用为父类,new出的实例化对象为子类
形参里是父类的对象引用,可以用子类的实例化对象去赋值
多态的使用:虚拟方法调用
有了对象的多态性以后,我们在编译期,只能调用父类中声明的方法,但在运行期,我们实际执行的子类重写父类的方法。不能调用子类的特有的方法,只能调用父类中声明的属性和方法,在运行的时候执行的是子类重写父类的方法,如果子类没有重写父类这个方法,那么执行的仍然是父类这个方法,因为编译时,左边仍然是父类的类型
总结:编译看左边,运行看右边(这里指的是方法,属性仍然都是看左边)
多态性的使用前提:
- 要有类的继承关系,有了继承才能谈这个多态性,没有继承就没有多态性。
- 子类要有重写父类的方法。
对象的多态性只适用于方法,不适用于属性!
想造对象就得造构造器
有了对象的多态性以后,内存中实际上是加载了子类特有的属性和方法的,但是由于变量(对象引用)声明为父类类型,导致编译时,只能调用父类中声明的属性和方法,子类的属性和方法不能调用。
如何才能调用子类特有的属性和方法?
首先编译器看到的左边的对象变量不能是父类类型的。
使用强制类型转换
Person p1 = new Man(); Man m1 = (Man)p1;
这叫向下转型,回想前面的强制类型转换,自动类型提升。右边是父类,左边是子类,那么右边要赋值给左边,就要用向下转型,是强制类型转换,第一行可以看作是自动类型提升。
这个时候拿m1就可以调子类特有的方法了
使用强转时,可能出现ClassCastException的异常
为了避免这个问题,引入了关键字
instanceof
三、其它关键字
this super static final abstract import等
this
this关键字的使用
this可以理解为当前对象
this可以用来修饰属性、方法、构造器
this.name理解为当前对象的属性name(属性是成员变量)
this修饰属性和方法:
this理解为当前对象
在类的方法中,我们可以使用“this.属性”或“this.方法”的方式,调用当前对象属性或方法。但是通常情况下,我们都选择省略this,特殊情况下,如果方法的形参和类的属性同名时,我们必须显示地使用“this.变量”的方式,表明此变量是属性(成员变量),而非形参(局部变量)
在构造器中,是同样的使用方式。
方法内可以调同类的其他方法,这个调其他方法这一步就可以写成this.方法,当前对象的其他方法。
this修饰构造器
- 我们在类的构造器中可以显示地使用“this(形参列表)”方式,调用本类中指定的其他构造器,具体调用的是哪个构造器,看形参列表。
- 构造器中不能通过this(形参列表)的方式调用自己。
- 如果一个类中有n个构造器,则最多有n - 1个构造器中使用了“this(形参列表)”方式
- 规定:“this(形参列表)”方式调用构造器,必须声明在当前构造器中的首行
- 构造器内部,最多只能声明一个“this(形参列表)”方式,用来调用其他构造器
- 当构造器需要重载的时候,就可以考虑用这种方式,用来降低代码的冗余,因为程序会要求实例化类的时候,要初始化对象,那么就要求每个构造器中都要写一个相同的方法,使得不管用哪个构造器都能够对对象进行相同的初始化,这种情况就this在构造器种调用别的构造器,就可以降低代码的冗余
对象排序可以用compare
对象排序也是考虑对象里面的属性来排序
package、import
package关键字的使用
- 为了更好地实现项目中类的管理,提出包的概念。package,在一个项目中创建多个不同的包,按照功能去区分,写的类就放在不同的包下
- 使用package声明类或接口所属的包,声明在源文件的首行。源文件是.java文件,源文件里可以有多个类,但是只有一个主类。
- 包,属于标识符,遵循标识符的命名规则、规范。需要见名知意。都是小写。
- 每
.
一次,就代表一层文件目录 - 补充:同一个包下不能命名同名的接口、类,不同的包下,可以命名同名的接口、类。文件目录下本身就不允许定义同名文件
MVC设计模式
模型层:model,主要处理数据
控制层:controller 处理业务逻辑
视图层:view 显示数据(和用户界面相关的)
import关键字的使用
import:导入
- 在源文件中,显式地使用import结构导入指定包下的类、接口
- 声明在package和类的声明之间!
- 如果需要导入多个结构,则并列写出即可。
- 可以使用
xxx.*
的方式表示可以导入xxx
下的所有结构 - 如果使用的类或接口是
java.lang
包下定义的,则可以省略import - 如果使用的类或接口是本包下定义的,则也可以省略import接口
- 如果在源文件中,使用了不同包下的同名类,则必须至少有一个类需要以全类名的方式显示
包名.包名.类名
- import static:导入指定类或接口中的静态结构:属性或方法。
在一个类当中,声明另一个类的变量,把这两个类的关系叫做关联关系
instanceof
关键字x instanceof A
检验x是否为类A的对象,返回值类型为boolean
Person p1 = new Man(); Man m = (Man)p1; // 强制转型。这时候就可以用m来调子类中特有的方法了 Woman wm = (Woman)p1;// 会报ClassCastException的异常
为了不让报这个异常,引入了instanceof关键字
instanceof
的使用x instanceof A
检验x是否为类A的对象,返回值类型为boolean,如果是返回true,如果不是,返回false
使用情景:为了避免向下转型时,出现异常,在向下转型之前 ,先进行instanceof的判断,返回true,再进行向下转型。
如果 a instance of A为true,那么a instance of A或A的父类 一定为true
final
final可以用来修饰的结构
- 类、方法、变量
final修饰一个类:
final class FinalA{}
不能被继承,比如说:String类、System类、StringBuffer类
final修饰一个方法
**表明该方法不能再被重写了!**比如Object类中的getClass()
final修饰一个变量
这里变量包括成员变量和局部变量
说明变量不可以被修改,此时被修饰的这个变量叫做常量
final修饰属性,可以考虑赋值的位置有:
- 显式初始化
- 代码块中初始化
- 构造器中初始化,可以在多个构造器中初始化,调用构造器是创建对象,此时已经在内存的堆空间中加载好了,并且有值了,而且不能修改了,那么调用的是哪个构造器,就是哪个构造器中初始化的值
final修饰局部变量:
- 尤其是使用修饰形参时,表明此形参是常量,当调用此方法时,给常量赋一个实参,一旦赋值以后,就只能在方法体内使用此形参,但不能进行修改
static final用来修饰属性:全局常量
接口中的属性都是static final
四、Object类的使用
Object类是所有Java类的根父类,如果没有显示extends,则默认继承Object类
Object类中的功能(属性、方法)就具有通用性。
Object类只声明了一个空参的构造器
程序员可以通知
System.gc()或者Runtime.getRuntime().gc()
来通知系统进行垃圾回收,会有一些效果,但是系统是否进行垃圾回收依然不确定。垃圾回收机制只回收JVM堆内存里的对象空间。
永远不要主动调用某个对象的finalize方法。交给垃圾回收机制来调用
==
和equals
的区别(高频面试题)回顾
==
的使用这是一个运算符,
equals
是方法。==
可以使用在基本数据变量和引用数据类型变量中。- 使用在基本数据类型变量中,就是比较的两个变量保存的数据!!(不一定类型非得一样,比如说char和int也可以比较)
- 使用在引用数据类型中,比较的是两个变量(对象引用)的地址值!!(就看两个对象引用是否指向的是同一个对象实体)
==
使用的时候必须保证符号左右两边变量类型一致。
int i = 10; int j = 10; double d = 10.0; System.out.println(i == j);// true System.out.println(i == d);// true char c = 10; System.out.println(i == c);// true char c1 = 'A'; char c2 = 65; System.out.println(c1 == c2);// true
基本数据类型,关注数值
equals()
使用- 是一个方法,而非运算符
- 方法要想被调用,那么就是通过对象来调用,说明
equals()
方法不能被使用在基本数据类型中equals()
方法只适用于引用数据类型。
Customer cust1 = new Customer("Tom", 21); Customer cust2 = new Customer("Tom", 21); System.out.println(cust1.equals(cust2)); // false String str1 = new String("atguigu"); String str2 = new String("atguigu"); System.out.println(str1.equals(str2)); // true
首先第三行代码是用到了多态,因为Customer这个类里并没有定义equals方法,equals方法是Object类的,所以cust1这个对象也可以用
Object的equals方法是
equals(obj)
括号里是父类,传入的是子类对象,这就是多态的使用
第三行结果是false。
第六行结果是true
原因是:
**Object类中equals方法本来也是比较的地址值!**但是String类将Object类的equals进行过重写。
Object类中equals()的定义:
public boolean equals(Object obj) { return (this == obj); }
说明:Object类中定义的equals方法和==的作用是相同的。
像String、Date、File、包装类等都重写了Object类中的
equals()
方法,重写之后,比较的就不是两个变量(对象引用)的地址值是否相同,而是比较两个对象引用的实体内容(指的内部的属性值!)是否相同。
通常情况下,自定义类如果使用equals()的话,也通常是比较两个引用对象的实体内容(属性)是否相同,那么我们就需要对equals方法进行重写。
自定义类应该如何去重写
equals
方法。看String类的重写源码,很简单。实际开发当中,不需要自己去手写,可以自动生成。
toString()
当我们输出一个对象引用时,实际上就是调用当前对象的toString()方法
打印出来的地址值并不是真实的内存地址,是JVM的虚拟的内存地址。
像String、Date、File、包装类等都重写了Object类中的toString()方法。
使得在调用对象的toString()方法时,返回实体内容信息,而不是返回地址值。所有打印出来的并不是对象引用实际的值(对象引用实际的值不是null就是地址值)
System.out.println(对象引用);
System.out.println(对象引用.toString());
是同一个意思
自定义类也可以重写toString()方法,当调用此方法时,返回对象的实体内容
toString()
和equals()
都可以通过IDE自动生成,不用自己去手动写。单元测试
想测试哪块代码,就单独地测试哪块代码。
五、包装类的使用
也可以翻译成封装类
针对八种基本数据类型定义相应的引用类型---包装类(封装类)
有了类的特点,就可以调用类中的方法,Java才是真正的面向对象。其实面向对象这一块是不包括基本数据类型的。
就是把基本数据类型变量封装在类中
基本类型、包装类、String的相互转换
java提供了8种基本数据类型对应的包装类,使得基本数据类型的变量具有类的特征
基本数据类型---->包装类
- 调用包装类的构造器
- 自动装箱
包装类---->基本数据类型
- 调用包装类的xxxValue
- 自动拆箱
JDK5.0新特性:自动装箱与自动拆箱
自动装箱:
int num1 = 10; Integer in1 = num1; boolean b1 = true; Boolean b2 = b1;
自动拆箱:
int num3 = in1;
基本数据类型、包装类----->String类型
// 1. 方式1---连接运算 int num1 = 10; String str1 = num1 + ""; // 其他几种数据类型也可以通过这种方式转换 // 2. 调用String重载的valueOf(Xxx xxx) float f1 = 12.3f; String s = String.valueOf(f1); Double d1 = new Double(12.3); String s1 = String.valueOf(d1); // 这里是先用了自动拆箱。
String类型----->基本数据类型、包装类
// 1. 调用包装类中的parseXxx方法。 String str1 = "123"; int a = Integer.parseInt(str1);
面试题:
这道题是涉及到自动类型提升。
知识点是:Integer内部定义了IntegerCache结构,IntegerCache定义了Integer[],保存了从-128到127范围的整数,如果我们使用自动装箱的方式,给Integer赋值的范围在-128到127这个范围,可以直接使用数组中的元素,不用再去new了。目的:提高效率。
所以m==n比较的是地址,而它们的地址相同,所以为true,第三题超过了127,那么自动装箱相当于是new了Integer对象,那么地址当然就不一样了。
六、static
当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象,只有通过new关键字才产生实际的对象,这时候系统才会在内存中给对象分配空间,其方法才可以供外部调用。
我们有时候希望无论是否产生了对象或者产生了多少对象的情况下,某些特定的数据在内存空间里只有一份!,这些数据不是随着new对象的时候,才存储在堆空间中,不然每new一次,那么堆空间中就会分配新的空间。这些数据是随着类的创建而存在于内存中了。
static修饰的变量就不归某一个具体的对象所有了,而是大家共享的。
static关键字的使用
static:静态的。静态变量 = 静态属性 =类变量
static可以用来修饰:属性、方法、代码块、内部类。不能修饰构造器
static修饰属性
叫静态变量(静态属性)(static修饰不了局部变量)
属性按是否使用static修饰分为静态属性和非静态属性(实例属性)
实例变量: 我们创建了类的多个对象,每个对象都独立地拥有一套类中的非静态属性,当修改其中一个对象的非静态属性时,不会导致其他对象中的属性值的修改
静态变量:我们创建了类的多个对象,多个对象共享同一个静态变量,当通过某一个对象修改静态变量时,会导致其他对象调用此静态变量时,是修改过了的。
说明:
- 静态变量随着类的加载而加载。
- 静态变量的加载要早于对象的创建。
- 可以通过
类.属性
的方式调用静态变量(也可以通过对象.属性的方式,但是没有必要) - 由于类只会加载一次,则静态变量在内存中也只会存在一份。存在于方法区的静态域中
静态变量举例:
System.out
Math.PI
static修饰方法
随着类的加载而加载,可以通过
类.方法
的方式进行调用静态方法中,只能调用静态的方法或属性(静态方法中,要想调用非静态的方法,就要实例化对象)
非静态方法中,既可以调用非静态的方法或属性,也可以调用静态的方法或属性。
静态结构完全和类的生命周期相同。
晚出生的可以调早出生的,早出生的不能调晚出生的。
static注意点:
在静态的方法内,不能使用this关键字、super关键字
因为this指的是当前对象,而static的方法和属性是随类的加载而加载,可能对象都还没有
凡是看到静态结构,前面没有声明的,省略的都是类名
在开发中,如何确定一个属性是否要声明为static
属性可以被多个对象所共享的,不会随着对象的不同而不同的,就声明为static
类中的常量也常常声明为static,因为常量基本就是被所有对象共享的。
在开发中,如何确定一个方法是否要声明为static
操作静态属性的方法,通常设置为static
工具类里的方法习惯上声明为static,比如:Math、Arrays、Collections
final修饰的变量就不是变量了,是一个常量
静态属性不会在构造器中去进行操作
设计模式是在大量的事件中总结和理论化之后优选的代码结构、编程风格以及解决问题的思考方式。
什么是单例设计模式:
类的单例设计模式,就是只让这个类造一个对象!单例就是单个实例
如何实现单例模式:
如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器的访问权限设置为private,这样就不能在类的外部用new操作符创建对象了。但是在类的内部仍然可以产生该类的对象(就像在main方法中想要调用非静态方法,那么先创建本类的对象,没有说创建类的对象一定要在外部进行)。
因为在类的外部无法创建对象,只能调用该类的某个静态方法返回类的内部创建的对象。静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的。
单例模式:
public class SingletonTest { public static void main(String[] args) { Bank bank = Bank.getInstanceBank(); } } // 饿汉式单例模式 class Bank { // 1. 将构造器私有化,避免在Bank类的外部调用new Bank() private Bank() { } // 2. 内部创建类的对象 // 4. 要求此对象引用也必须声明为静态的。 private static Bank instanceBank = new Bank(); // 3. 提供公共的静态的方法 public static Bank getInstanceBank() { return instanceBank; } }
public class SingletonTest2 { public static void main(String[] args) { Order order1 = Order.getOrderInstance(); Order order2 = Order.getOrderInstance(); System.out.println(order1 == order2); } } // 懒汉式单例模式 class Order { // 1. 私有化类的构造器 private Order() {} // 2. 声明当前类的对象引用 // 4. 此对象引用必须声明为static private static Order orderInstance = null; public static Order getOrderInstance() { if (orderInstance == null ) { orderInstance = new Order(); } return orderInstance; } }
饿汉式和懒汉式的区别:
饿汉式:
好处:线程安全!
坏处:对象加载时间过长
懒汉式:
- 好处:延迟对象的创建
- 目前的写法的坏处是线程不安全
java.lang.Runtime
就是典型的单例模式,单例模式减少了系统性能开销(从创建对象的角度来说)在应用启动时,直接产生一个单例对象,然后永久地驻留在内存中。
在一个源文件中可以有多个类,但是只有一个类能够声明为public
main方法的使用说明
- main()方法作为程序的入口
- main()也是一个普通的静态方法
- main()方法也可以作为我们与控制台交互的一种方式(之前,使用Scanner)
七、代码块
代码块的作用:
用来初始化当前的类或对象
代码块如果有修饰的话,只能是static
分类:静态代码块 vs 非静态代码块
静态代码块:
内部可以有输出语句
随着类的加载而加载而且会执行且只执行一次(因为类只会加载一次),静态方法只是随着类的加载而加载,但是并不执行方法体中的语句
初始化类的信息。对静态变量进行赋值
如果在一个类中定义了多个静态代码块,则按照声明的先后顺序执行
静态代码块的执行,要优先于非静态代码块的执行,因为随着类的加载而加载和随着创建对象而加载,还是考虑生命周期。
静态代码块内只能调用静态的属性、静态的方法,不能调用非静态的结构
非静态代码块:
内部可以有输出语句
随着对象的创建而执行
每创建一个对象,就执行一次非静态代码块
可以在创建对象时,对对象的属性等进行初始化
如果在一个类中定义了多个非静态代码块,则按照声明的先后顺序执行
非静态代码块可以调用静态的属性和方法,也可以调用非静态的属性和方法。
对属性可以赋值的位置
- 默认初始化
- 显式初始化
- 构造器中初始化
- 有了对象以后,通过对象.属性或对象.方法(set方法)的方式,进行赋值
- 代码块中赋值
八、抽象类与抽象方法
抽象类用abstract修饰,明确这个类不再造对象了,表明这个类不能再实例化了。
abstract
关键字的使用abstract
可以用来修饰的结构:类、方法abstract修饰类:抽象类
- 此类不能再实例化(这也是加abstract的目的),但是不代表抽象类内部的构造器没有用,我们说构造器有两个作用,一是实例化对象,二是初始化对象,虽然不能被实例化对象,但是这个构造器仍然有用,是因为子类实例化的时候仍然会调父类的构造器,这是子类实例的创建过程。
- 抽象类中一定有构造器,便于子类实例化的时候调用。
- 开发中,都会提供抽象类的子类,让子类对象实例化,完成相关的操作。
abstract修饰方法
只有方法的声明,没有方法体,没有大括号。
包含抽象方法的类一定是抽象类,反之抽象类中可以没有抽象方法的
若子类重写了父类(包括直接父类和间接父类)中的所有的抽象方法,此子类方可实例化
若子类没有重写父类中的所有抽象方法,则此子类也是一个抽象类,需要用abstract修饰
abstract使用上的注意点:
- abstract只能用来修饰类和方法,不能修饰属性和构造器,static不能修饰构造器
- abstract不能用来修饰私有方法,因为abstract修饰的方法就是用来子类重写的,但是private方法是不可以被重写的。
- abstract不能用来修饰静态方法。因为子父类中同名同参数的方法如果都是static的,这不叫重写。从方法是否能够重写这个角度来考虑
- abstract不能用来修饰final修饰的方法、final的类
抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。
九、接口
有时必须从几个类派生出一个子类,继承他们所有的属性和方法,但是Java不支持多重继承,Java支持多层继承,有了接口,就可以得到多重继承的效果。
一个类可以实现多个接口,一定程度上解决了类的单继承性
接口就是规范,定义的是一组规则
继承强调“是不是”的关系,接口强调“能不能”的关系
接口的使用:
接口使用interface来定义
在java中,接口和类是并列的结构
如何定义接口:定义接口的成员
JDK7及以前:只能定义全局常量和抽象方法
全局常量:public static final
抽象方法:public abstract的(abstract不能修饰private方法,因为需要重写)
JDK8:除了定义全局常量和抽象方法之外,还可以定义静态方法和默认方法
静态方法和默认方法里还可以写方法体。abstract修饰的方法就是需要实现类去实现,子类去重写。
接口中不能定义构造器,意味着接口不能实例化
之前说抽象类也不能实例化,但是抽象类中有构造器,是因为子类可以实例化,子类实例化会调用父类的构造器。因为构造器中如果既没有写this,也没有写super,则默认是super()
Java开发中,接口都通过类去实现(implements)的方式来使用,如果实现类覆盖了接口中的所有抽象方法,则此实现类可以实例化,如果实现类没有覆盖接口中的所有抽象方法,则此实现类仍然是抽象类。
类是实现接口中的方法,重写抽象类中的方法。但是其实是一个意思,都是覆盖。
java类可以实现多个接口,弥补了Java单继承性的局限性。
接口与接口之间可以继承,而且可以多继承
接口的具体使用,体现多态性。
接口的主要用途就是被实现类实现
与继承关系类似,接口与实现类之间存在多态性
实现接口和继承父类一样,接口和父类中定义的变量都可以直接拿来用
接口里不写方法体,但是规范是已经定义好的
JDK8中的接口:
接口中定义的静态方法,只能通过接口调用
接口中定义的默认方法, 可以通过声明接口的实现类的实例化对象来调用。
如果实现类重写了接口中的默认方法,调用时仍然调用的是重写以后的方法。
如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的方法,那么子类在没有重写此方法的情况下,默认调用的是父类的这个同名同参数的方法----- 类优先原则。
如果实现类实现了多个接口,而这多个接口中定义了同名同参数的方法,那么在实现类没有重写此方法的情况下,报错---->接口冲突。
这就需要我们必须在实现类中重写此方法。
如何在子类或实现类的方法中调用父类、接口中被重写的方法。
Collections是操作集合的工具类。
抽象类和接口有哪些共同点和区别:
相同点:不能实例化,都可以被继承
不同点:
抽象类:有构造器,接口不能声明构造器
接口可以多继承,类只能单继承
接口更强调于规范,通过抽象的方法来体现。
是不是 vs 能不能
jdk8以后可以添加静态方法和默认方法。
面试问到这种题可以把概念都说一说。
十、内部类
java中允许将一个类A声明在另一个类B中,则类A就是内部类,类B就是外部类
内部类的分类:成员内部类(静态、非静态) vs 局部内部类(方法内、代码块内、构造器内)
成员内部类就是直接定义在类里面、方法外的。
成员内部类:
一方面,作为外部类的成员:
- 调用外部类的结构
- 可以被static修饰(外部类不可以,但是内部类可以)
- 可以被四种不同的权限修饰。
另一方面,作为一个类:
- 类内可以定义属性、方法、构造器等
- 可以被final修饰,表示此类不能被继承,言外之意,不使用final,就可以被继承。
- 可以被abstract修饰,表明这个内部类不能被实例化
关注如下的三个问题:
如何实例化成员内部类的对象
如何在成员内部类中区分调用外部类的结构
开发中局部内部类的使用
六、异常
两个异常处理机制:
- try-catch-finally
- throws
在Java语言中,将程序执行中,发生的不正常情况叫做异常。
Java程序在执行过程中,所发生的异常事件可分为两类:
Error和Exception
Error是Java虚拟机无法解决的严重问题,一般不编写针对性的代码进行处理。
- 栈溢出:java.lang.StackOverFlowErroe
Exception:其他因编程错误或偶然的外在因素导致的一般性问题。可以使用针对性的代码进行处理
狭义上的异常指的是Exception
异常又分为编译时异常和运行时异常
编译时异常:(CheckedException)
- Java源程序转换为字节码文件这个过程中出现的异常
运行时异常:(RuntimeException)
- JVM加载字节码文件并运行这个过程中出现的异常。
异常的体系结构
异常的处理:抓抛模型
过程一:抛:程序在正常执行的过程中,一旦出现异常,就会在异常代码处生成一个对应异常类的对象,并将此对象抛出。抛至上一层。
一旦抛出对象以后,其后的代码就不再执行。
过程二:抓:可以理解为异常的处理的方式
- try-catch-finally
- throws(throws到上层也会catch的)
try-catch-finally
的使用try { // 可能出现异常的代码 } catch (异常类型1 变量名1) { // 处理异常的方式1 } catch (异常类型2 变量名2) { // 处理异常的方式2 } catch (异常类型3 变量名3) { // 处理异常的方式3 } finally { // 一定会执行的代码 }
finally是可选的
使用try将可能出现异常的代码包装起来,在执行过程中,一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,去catch中进行匹配。
一旦try中的异常对象匹配到某一个catch时,就进入catch中进行异常的处理,一旦处理完成,就跳出当前的try-catch结构,继续执行其后的代码。
catch中的异常类型如果没有子父类关系,则谁声明在上,谁声明在下无所谓。
catch中的异常类型如果满足子父类关系,则要求子类一定声明在父类的上面,否则报错。
catch里常写的两种处理方式:
e.printStackTrace(); String msg = e.getMessage();
在try结构中声明的变量,在出了try结构以后,就不能调用了。
try-catch-finally结构可以嵌套
finally中声明的是一定会被执行的代码,即使catch中又出现异常了,try中有return语句,catch中有return语句等情况。而且finally中的代码在被try、catch中return语句之前先执行!这种情况如果finally语句中有return,那么try、catch中的return便不会被执行
像数据库连接、输入输出流、网络编程Socket等资源,JVM是不能自动回收的,我们需要自己手动地进行资源的释放。资源的释放操作就需要声明在finally中!那么finally中的代码就不会因为上面的代码出现异常而不会被执行。
开发中,由于运行时异常比较常见,所以我们通常就不针对运行时异常编写try-catch-finally了,针对于编译时异常,我们说一定要考虑异常的处理。运行时异常一般不处理
throws + 异常类型
“throws + 异常类型”写在方法的声明处,指明此方法执行时,可能会抛出的异常类型
一旦当方法体执行时,出现异常,仍会在异常代码处生成一个异常类的对象,此对象满足throws后的异常类型时,就会被抛出(throws),对当前这个方法来说,异常处理结束,实际上异常并没有被catch住,会抛向上一层,上一层是指调用这个方法这一层,最上层是main方法,就不可以再在main方法后写throws了,否则会抛出异常到虚拟机。如果一直往上抛,那么一定要在main方法内调用处进行try-catch
异常代码处,会生成异常类的对象,异常代码后续的代码就不再执行
体会:try-catch-finally:真正地将异常给处理掉
throws的方式只是将异常抛给了方法的调用者,并没有真正地将异常处理掉。
开发中如何选择try-catch-finally还是throws
- 如果父类中被重写的方法没有throws方式处理异常,则子类重写的方法也不能使用throws,意味着如果子类重写的方式中有异常,必须使用try-catch-finally
- 执行的方法a中,先后又调用了另外的几个方法,一层一层地。这几个方法是递进关系执行的,我们建议这几个方法使用throws的方式进行处理,而执行的方法a可以考虑使用try-catch-finally的方式进行处理
关于异常对象的产生:
- 系统自动生成的异常对象
- 手动地生成一个异常对象并抛出
throw
(throw
是产生异常对象的方法,throws
是处理异常的方法)
如何自定义异常类?
继承于现有的异常结构:RuntimeException、Exception
运行时异常一般不显式地去处理,所以抛出RuntimeException,不针对RuntimeException编写try-catch-finally来处理,抛出Exception,需要我们去处理这个异常。运行时异常不处理,编译时异常才处理
throw Exception就把编译的异常考虑在内了,所以必须处理!
运行时异常如果程序自动报错,不处理,如果我们手动抛出,也不处理。编译时异常才处理,即使手动抛出,也需要处理。
提供全局常量:serialVersionUID
提供重载的构造器,带参的和不带参的。
public class TeamException extends Exception { static final long serialVersionUID = -3366867896796l; public TeamException() { super(); } public TeamException(String msg) { super(msg); } }
编译时异常:执行javac.exe这个命令时可能出现的异常,比如说FileNotFoundException,让我们考虑文件可能不存在这个事情,并不是说文件一定不存在,只是让我们考虑这种可能并提前进行处理
运行时异常:执行java.exe这个命令时,出现的异常。
七、多线程
在Eclipse中我们有Workspace(工作空间)和project(工程)的概念,在IDEA中只有Project和module(模块)概念
不会说IDEA中的module就是一个项目,只是相当于Eclipse中的一个项目
创建多线程有4种方式
同步解决的是线程安全问题
同步展开来说是同步代码块和同步方法。
还有一种方式是lock(JDK5开始)
解决线程安全问题一共有3种方法。
程序是为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程,有它自身的产生、存在和消亡的过程,这个过程叫生命周期。
程序是静态的,进程是动态的。
进程是作为资源分配的单位。系统在运行时会为每个进程分配不同的内存区域
进程可以进一部细分为线程。若一个进程同一时间并行执行多个线程,就是支持多线程的。
线程是作为调度和执行的单位。每个线程拥有独立的运行栈和程序计数器。线程切换的开销小。
一个进程中的多个线程共享相同的内存单元/内存地址空间,他们从同一个堆中分配对象,可以访问相同的变量和对象,这就使得线程间通信更加简便、高效。
但是多个线程操作共享的系统资源可能会带来安全隐患。
方法区和堆是每个进程一份,每个线程都有独立的虚拟机栈和程序计数器。多个线程共享一个进程中的方法区和堆(所以线程之间的通信简便、高效)。
main方法对应的就是一条线程。
单核CPU是假的多线程,但是因为CPU单元时间特别短,感觉不出来
一个Java应用程序,java.exe, 其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程
当然如果发生异常,会影响主线程。
并行:多个CPU同时执行多个任务
并发:一个CPU(采用时间片)“同时”执行多个任务。
多线程的创建
方式一:
/** * 多线程的创建:方式一:继承于Thread类 * 1. 创建一个继承于Thread类的子类 * 2. 重写Thread类的run方法----->把我们要做的事写在重写的run方法中 * 3. 创建Thread的类的子类的对象(这个事情需要在主线程main()里面做) * 4. 通过此对象调用Thread类的start方法,我们自己写的子类并没有定义start方法,调的是父类的方法。 * * 例子: 遍历100以内的偶数 */ public class ThreadTest { public static void main(String[] args) { MyThread myThread = new MyThread(); // 我们不能通过直接调用run()方法的方式启动线程,否则还是单线程,都是main这个线程在执行。 myThread.start(); // 到这一步仍然是在主线程里执行。 for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(i + "*********"); } } } } class MyThread extends Thread { public MyThread () { } @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(i); } } } }
分析这个过程:一个JVM进程或者说一个.java程序包含三个线程,一个是main()主线程,一个是垃圾回收线程,还有一个是异常处理线程。我们这里只讨论主线程。主线程里,到myThread.start()这一行,都是主线程在执行,执行到这里,创建的myThread线程开始启动。在这之后是主线程里myThread.start()之后的代码和我们创建的线程myThread的run()里的代码是在同时执行的,这就是多线程执行!也就是主线程和我们创建的线程是在同时执行!
不可以让已经start的线程还去执行,不然会报异常。我们需要重新创建一个线程的对象,去start
start()方法:
- 启动当前线程(只能调start()方法才能启动线程!)
- 调用当前线程的run()方法,子类重写了父类的run()方法,所以调用的我们重写的run()方法
不同的线程执行的事情不一样,就各自去继承Thread类,写各自的方法 。
/** * 测试Thread中的常用方法 * 1. start():启动当前线程,调用当前线程的run() * 2. run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中 * 3. currentThread():静态方法,返回当前代码的线程 * 4. getName():获取当前线程的名字 * 5. setName(): 设置当前线程的名字 * 6. yield(): 释放当前CPU的执行权,有可能在下一刻又分配到这个线程 * 7. join(): 在线程a中调用线程b的join(),此时线程a进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。看什么时候分配到CPU,才继续执行。 * 8. sleep(): 强制让线程阻塞,阻塞之后,也不是说会立刻继续执行,阻塞之后仍然会等cpu分配资源,分配到了才继续执行。在阻塞期间,即使分配到CPU的资源,也不能执行。 线程的优先级: MAX_PRIORITY:10 MIN_PRIORITY:1 NORM_PRIORITY:5(默认的优先级) 如何获取和设置当前线程的优先级: 见下 说明:高优先级的线程要抢占低优先级线程CPU的执行权,但是只是从概率上来讲,高优先级的线程高概率的情况下被执行,并不意味着,只有当高优先级的线程执行完以后,低优先级的线程才执行。 */ public class ThreadMethodTest { public static void main(String[] args) { HelloThread helloThread = new HelloThread("线程1"); helloThread.start(); Thread.currentThread().setName("主线程"); for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(Thread.currentThread().getName()+ ":" + i); } if (i == 20) { try { helloThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } class HelloThread extends Thread { public HelloThread() {} public HelloThread(String name) { super(name); } @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(Thread.currentThread().getName()+ ":" + i); } } } }
线程的调度
线程执行的时候,涉及到CPU调度的策略,进程是资源分配的最小单位,而线程是调度和执行的最小单位
多线程的创建
方式二:
/** * 创建多线程的方式二:实现Runnable接口 * 1. 创建一个实现了Runnable接口的类(注意:这个类并不是Thread类。而是实现接口的类。不像之前继承Thread类) * 2. 实现类去实现Runnable中的抽象方法:run() * 3. 创建Runnable接口的实现类的对象 * 4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象。 * 5. 通过Thread类的对象去调用Thread类的start()方法:1.启动线程 2.调用当前线程的run()------->调用了Runnable类型的target的run() */ public class ThreadTest1 { public static void main(String[] args) { MThread mThread = new MThread(); Thread t1 = new Thread(mThread); Thread t2 = new Thread(mThread); t1.start(); t2.start(); } } class MThread implements Runnable { @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(Thread.currentThread().getName() + " : " + i); } } } }
比较创建多线程的两种方式:
开发中,优先选择实现Runnable接口的方式
原因:
- 实现的方式没有类的单继承性的局限性。
- 实现的方式更适合来处理多个线程有共享数据的情况。
联系:
- Thread类也实现了Runnable接口
- 两种方式都需要重写run()方法,将线程要执行的逻辑声明在run()中
线程的生命周期
- Thread类的内部类State定义了线程的多个状态
NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
线程的生命周期图
阻塞和就绪状态要区分开,就绪状态是线程进入线程队列,等待CPU分配资源,CPU分配了资源就可以执行这了,而阻塞状态是CPU分配给这个线程资源,想执行这个线程也执行不了。
阻塞一定不是线程的最终状态,如果线程阻塞了,始终回不到运行状态,是有问题的,死亡才是线程生命周期的终点。
阻塞状态不能马上回到运行状态,而是先回到就绪状态,也就是说,阻塞状态结束,仍然要等待CPU分配资源(时间片)才可以执行,这个阶段就是就绪状态。
阻塞状态的线程被唤醒后(notify()或notifyAll())是回到就绪状态,仍然要等待CPU分配时间片,而不能马上执行。
运行到阻塞状态:
sleep() join() 等待同步锁 wait() suspend()
线程的同步
线程的同步是解决线程的安全问题。
当一个线程a在操作某块资源的时候,其他线程不能参与进来,直到线程a操作完这块资源的时候,其他线程才可以操作这块资源,即使线程a在操作这块资源的时候出现了阻塞,其他线程也只能等着。
在Java中,我们通过同步机制,来解决线程的安全问题。
方式一:同步代码块
一:同步代码块解决实现Runable接口的方式的线程安全问题。
synchronized(同步监视器) { // 需要被同步的代码 }
说明:操作共享数据的代码,即为需要被同步的代码。
共享数据:多个线程共同操作的变量。(有共享数据才可能产生线程安全问题。)
同步监视器:俗称:锁。谁能拿到这个锁,谁就能进去操作这块代码。任何一个类的对象,都可以充当锁。
补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器
要求:**多个线程必须要共用同一把锁,这个锁就一个。**这个锁就好比是高铁上公共厕所的灯。
好处:使用了同步的方式,解决了线程安全的问题
局限性:操作同步代码时,只能有一个线程参与,其他线程等待,相当于是一个单线程的过程,效率低。但是即使是这样,仍然需要这种方式来解决线程安全问题。
二:同步代码块解决继承Thread方式的线程安全问题
就从创建对象的角度来分析,一定要保证只能有一把锁!,这种方式会造多个自定义Thread类的对象,所以会造多把锁,这时要把锁定义成静态的。保证只有一把。
说明:在继承Thread类创建多线程的方式中,慎用this充当同步监视器。考虑使用当前类来充当同步监视器。(当前类也可以认为是对象,类型为Class的对象,万事万物皆对象)
方式二:同步方法
一:同步方法解决实现Runable接口的方式的线程安全问题。
如果操作共享数据的代码完整地声明在一个方法中,我们不妨将此方法声明为同步的。
二:使用同步方法处理继承Thread类的方式中的线程安全问题
关于同步方法的总结:
同步方法仍然涉及到同步监视器,只是不需要我们显式地声明
非静态的同步方法,同步监视器是:this
静态的同步方法:同步监视器是:当前类本身
在继承Thread类的这种方式中,经常会创建多个自定义Thread类对象,为了保证同步监视器的唯一性,要将同步方法改为静态的。
死锁:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
我们使用同步时,要避免出现死锁。
Lock(锁)
解决线程安全问题的方式三:Lock锁,JDK5.0新增
public class LockTest { public static void main(String[] args) { Window6 window6 = new Window6(); Thread t1 = new Thread(window6); Thread t2 = new Thread(window6); Thread t3 = new Thread(window6); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } class Window6 implements Runnable { private int ticket = 100; // 1. 实例化ReentrantLock private ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { try { // 2. 调用lock()方法,相当于获取同步监视器。就锁住了。 lock.lock(); show(); } finally { // 3.调用解锁方法:unlock() lock.unlock(); } } } private void show() { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ": 卖票,票号为: " + ticket); ticket--; } } }
面试题:synchronized和lock的方式的异同?
相同点:他们都能用来解决线程安全问题。
不同点:synchronized是执行完之后,自动释放锁(同步监视器),lock必须手动启动同步(上锁),调unlock来手动结束同步(释放锁)。
线程的通信
涉及到的三个方法:
wait()
:一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器(这里要和sleep()区分,sleep是不会释放同步监视器的,sleep和wait都是线程调用,然后进入阻塞状态的方式。区别就在于sleep不释放同步监视器,而wait会释放。)notify()
:一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的那个。这里要注意,线程调用wait是使本线程进入阻塞状态,而notify是唤醒别的线程(唤醒的是被wait的线程而不是自身)notifyAll()
:一旦执行此方法,就会唤醒所有被wait的线程说明:
- 以上这三个方法只能出现在同步代码块和同步方法中。
- 以上这三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
- 以上这三个方法是定义在
java.lang.Object
中的。因为同步监视器任何一个对象都可以充当,结合第二点,任何一个对象都必须能够调用以上三个方法,说明,任何一个对象都有这三个方法,所以这三个方法是声明在Object类中。
sleep()和wait()方法的异同?
相同点:
- 一旦执行到这两个方法,都可以使得当前的线程进入等待状态
不同点:
- 两个方法声明的位置不一样,Thread类中声明sleep()方法,还是静态的,Object类中声明wait()
- 调用的范围不同,sleep的调用没有任何要求,可以在任何需要的场景下调用,而wait()一定要在同步代码块和同步方法中才能调用。
- 如果两个方法都使用在同步代码块和同步方法中,sleep()不会释放同步监视器,wait()会释放同步监视器。
notify()
方法将等待队列的一个等待线程从等待队列中移动到同步队列中,而notifyAll()
方法则是将等待队列中所有等待线程全部移动到同步队列,被移动的线程状态从等待状态变成阻塞状态,得到监视器锁之后,得到锁的线程才会变成就绪状态,分配到CPU时间片才会执行,变成运行态。生产者/消费者问题
/** * 线程通信的应用:生产者/消费者问题 * 分析: * 1. 是否是多线程问题?是,生产者线程,消费者线程。 * 2. 是否有线程安全问题?是,店员(或产品) * 3. 如何解决线程的安全问题?同步机制,有三种方法。 * 4. 是否涉及到线程的通信?是。 * */ public class ProductTest { public static void main(String[] args) { Clerk clerk = new Clerk(); Producer p1 = new Producer(clerk); p1.setName("生产者"); Consumer c1 = new Consumer(clerk); c1.setName("消费者1"); Consumer c2 = new Consumer(clerk); c2.setName("消费者2"); p1.start(); c1.start(); c2.start(); } } class Clerk { private int productNum = 0; public synchronized void produceProduct() { if (productNum < 20) { productNum++; System.out.println(Thread.currentThread().getName() + ": 开始生产第" + productNum + "个产品"); this.notify(); } else { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void consumeProduct() { if (productNum > 0) { System.out.println(Thread.currentThread().getName() + ": 开始消费第" + productNum + "个产品"); productNum--; this.notify(); } else { //等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Producer extends Thread { private Clerk clerk; public Producer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { System.out.println(Thread.currentThread().getName() + ": 开始生产产品"); while (true) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } clerk.produceProduct(); } } } class Consumer extends Thread { private Clerk clerk; public Consumer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { System.out.println(Thread.currentThread().getName() + ": 开始消费产品"); while (true) { try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } clerk.consumeProduct(); } } }
JDK5.0新增创建多线程方式
方式三:使用Callable接口
- 与使用Runnable相比,Callable功能更强大些
- 相比run()方法,call()可以有返回值
- call()方法可以抛出异常,run()里面有异常只能是try-catch,因为Thread类里的run方法没有抛出异常,所以子类也不可以。子类所抛出的异常的类型不能大于父类所抛出的异常的类型,子类只有重写的方法的权限修饰符是不小于父类的方法的权限修饰符。
- 支持泛型的返回值
- 需要借助FutureTask类或者Future接口,比如获取返回结果
/** * 创建线程的方式三:实现Callable接口---jdk5.0新增 */ public class ThreadNew { 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() Thread thread = new Thread(futureTask); thread.start(); try { // get()方法的返回值即为FutureTask构造器Callable对象的实现类所重写的call()方法的返回值 Object sum = futureTask.get(); System.out.println("总和为: " + sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } // 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 = sum + i; } } return sum; } }
方式四:使用线程池
public class ThreadPool { public static void main(String[] args) { // 1. 提供指定线程数量的线程池 ExecutorService executorService = Executors.newFixedThreadPool(10); NumberThread numberThread1 = new NumberThread(); NumberThread1 numberThread2 = new NumberThread1(); // 2. 执行指定的线程的操作,需要提供实现Runnable接口或Callable接口的对象 executorService.execute(numberThread1); // 适合适用于Runnable executorService.execute(numberThread2); executorService.shutdown(); // executorService.submit(); // 适合使用于Callable } } 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); } } } }
八、常用类
String、StringBuffer、StringBuilder
String声明为final,不可被继承
String实现了Serializable接口,表示字符串是支持序列化的。
实现了Comparable接口,表示String可以比较大小
String类内部定义了final char[] value 用于存储字符串数据,final表示数组不可以被重新赋值,数组的元素也不可以修改了。
String:代表一个不可变的字符序列。这是String的不可变性。
- 体现:
- 当对字符串重新赋值时,需要重新指定内存区域赋值,不能使用原有的value进行赋值,因为原有的value时final的,在底层体现为不可变的字符序列。
- 当对现有的字符串进行连接操作时,也需要重新指定内存区域进行赋值,不能在原有的value上进行赋值。
- 当调用String的replace()方法时,仍然需要重新指定内存区域进行赋值,不能在原有的value上进行赋值。
- 体现:
通过字面量的方式(区别于new方式)给一个字符串赋值,此时的字符串值声明在常量池中
字符串常量池中是不会存储相同内容的字符串的。
面试题:
String s = new String("abc");
方式创建对象,在内存中创建了几个对象?两个:一个是堆空间中new结构,另一个是char[]对应的字符串常量池中的数据"abc"
结论:
- 常量与常量的拼接结果在常量池,且常量池中不会存在相同内容的常量
- 只要其中有一个是变量(没有加final),结果就在堆中
- 如果拼接的结果调用intern()方法,则返回的结果在常量池
虽然JVM规范将方法区描述为堆的一个逻辑部分,但它还有一个别名叫做Non-Heap,目的就是要和堆分开。
复习:
String 与基本数据类型、包装类之间的转换
String --> 基本数据类型、包装类:调用包装类的静态方法:parseXxx(str)
基本数据类型、包装类 --> String: 调用String重载的ValueOf()方法
String 与 char[]之间的转换
String --> char[] : toCharArray()
char[] --> String :调用String的构造器
String是不可变的字符序列,字面量赋值的方式是在常量池中,new的方式仍然是在堆中。
StringBuffer和StringBuilder是可变的字符序列。
关于StringBuffer和StringBuilder的使用
String、StringBuffer、StringBuilder三者的异同
String:是不可变的字符序列,底层使用char[]存储
StringBuffer:可变的字符序列,线程安全的,效率低。底层也是用char[]存储的,但是没有用final修饰
StringBuilder:可变的字符序列,jdk1.5新增,线程不安全,效率稍高。底层也是用char[]存储的,但是没有用final修饰
StringBuilder里面的方法和StringBuffer里的方法都是差不多的,只是没有synchronized关键字。
源码分析:
String str = new String();// char[] value = new char[0]; String str1 = new String("abc");// char[] value = new char[]{'a', 'b', 'c'} // 如果是调用的空参的构造器,则创建了一个长度为16的数组 StringBuffer sb1 = new StringBuffer();//char[] value = new char[16] System.out.println(sb1.length());//0 sb1.append('a');// value[0] = 'a'; sb1.append('b');// value[1] = 'b'; StringBuffer sb2 = new StringBuffer("abc");// char[] value = new char["abc".length() + 16]; System.out.println(sb2.length());//3 //问题2:扩容问题,如果要添加的数据底层数组存不下了,那就需要扩容 // 默认情况下,扩容为原来容量的2倍 + 2, 同时将原有数组中的元素赋值到新的数组中。
指导意义:开发当中,建议大家使用
StringBuffer(int capacity)或StringBuilder(int capacity)
一些类重写过了Object的toString方法,String、包装类、File、Date都重写过了toString()方法。
日期、时间
Date类
两个构造器:
- 空参构造器,创建对应当前时间的Date对象
- 参数为long型变量的构造器,创建指定毫秒数的Date对象
两个方法:
- toString() 显式当前的年、月、日、时、分、秒
- getTime() 获取当前Date对象对应的时间戳(毫秒数)
new的是子类,赋给父类的对象引用,再强转下来,这是可以的。比如
Person p = new Man(); Man man = (Man) p;
但是如果new的就是父类,强转给子类,这是不可以的。比如:
Person p = new Person(); Man man = (Man) p;
SimpleDateFormat
/** * 1. 两个操作 * 1.1 格式化: 日期----> 字符串 * 1.2 解析:格式化的逆过程,字符串----->日期 */ public void testSimpleDateFormat() throws ParseException { // 使用默认的构造器 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(); // 格式化: 日期----> 字符串 Date date = new Date(); String format = simpleDateFormat.format(date); System.out.println(format); // 解析:格式化的逆过程,字符串----->日期 String str = "2021/12/16 下午5:28"; Date date1 = simpleDateFormat.parse(str); System.out.println(date1); // *************按照指定的方式格式化和解析:调用带参的构造器******************* System.out.println("**********************"); // 使用带参的构造器 SimpleDateFormat simpleDateFormat1 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); // 格式化 String format1 = simpleDateFormat1.format(date); System.out.println(format1); // 解析 String str1 = "1999-11-11 12:12:12"; Date date2 = simpleDateFormat1.parse(str1); System.out.println(date2); }
Java比较器
说明:Java中的对象,正常情况下,只能进行比较
==
或!=
,不能使用>
或<
, 但是在开发的场景中,我们需要对多个对象进行排序,言外之意,我们需要比较对象的大小。(==
对于引用类型变量,是比较他们的地址值,对于基本数据类型才是比较内容。对于对象,==
比较的当然是地址值。)如何实现:使用两个接口中的任何一个,Comparable或Comparator
Comparable
的使用(自然排序)String实现了这个接口,说明String的对象可以比较大小。
既然String实现了这个接口,那么就得重写这个接口里的方法。这个接口里有一个方法叫compareTo(),String就重写了这个compareTo()方法来实现比较两个字符串的大小。
像String、包装类等实现了Comparable接口,重写了compareTo()方法,给出了比较两个对象大小的方式。
重写compareTo()的规则:
如果当前对象this大于形参对象obj,则返回正整数, 如果当前对象this小于形参对象obj,则返回负整数, 如果当前对象this等于形参对象obj,则返回零。
当前对象可以理解为调用此方法的对象!
对于自定义类来说,如果需要排序,我们可以让自定义类实现Comparable接口,重写compareTo(obj)方法。
在compareTo(obj)方法中指明如何排序,考虑对象的属性,通过对象的属性来进行排序。
Comparator
的使用(定制排序)String实现Comparable接口,自动从小到大排,我们想让其从大到小排,这就是实现定制了。
背景:
当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码,或者实现了java.lang.Comparable接口的排序规则不适合当前的操作,那么可以考虑使用 Comparator 的对象来排序
既然实现这个接口,那么就要实现(重写)这个接口的抽象的compare方法
重写compare(Object o1,Object o2)方法,比较o1和o2的大小: 如果方法返回正整数,则表示o1大于o2; 如果返回0,表示相等; 返回负整数,表示o1小于o2。
Comparable接口与Comparator的使用的对比:
Comparable接口的方式一旦一定,保证Comparable接口实现类的对象在任何位置都可以比较大小。Comparator接口属于临时性的比较。
垃圾回收是一个独立的线程
九、枚举类与注解
枚举类的使用
当需要定义一组常量时,强烈建议使用枚举类。对象是有限个并且是确定的。
枚举类的理解:类的对象只有有限个,确定的。我们称此类为枚举类
如果枚举类中只有一个对象,则可以作为单例模式的实现方式。
如何定义枚举类:
- jdk5.0之前,自定义枚举类。
- jdk5.0,可以使用enum关键字定义枚举类
public class SeasonTest { public static void main(String[] args) { // 拿枚举类的对象做赋值 Season season = Season.SPRING; System.out.println(season.toString()); } } class Season { //1. 声明Season对象的属性 private final String seasonName; private final String seasonDesc; // 2. 私有化类的构造器 private Season(String seasonName, String seasonDesc) { this.seasonName = seasonName; this.seasonDesc = seasonDesc; } //3. 提供当前枚举类的多个对象。public static final public static final Season SPRING = new Season("春天", "春暖花开"); public static final Season SUMMER = new Season("春天", "夏日炎炎"); public static final Season AUTUMN = new Season("春天", "秋高气爽"); public static final Season WINTER = new Season("春天", "冰天雪地"); // 4. 其他诉求:获取枚举类对象的属性 public String getSeasonName() { return seasonName; } public String getSeasonDesc() { return seasonDesc; } // 5. 提供toString()方法,否则打印当前对象,仍然是地址值 @Override public String toString() { return "Season{" + "seasonName='" + seasonName + '\'' + ", seasonDesc='" + seasonDesc + '\'' + '}'; } }
/** * 使用enum关键字定义枚举类 * 说明:定义的枚举类默认继承于java.lang.Enum类。 */ public class SeasonTest1 { public static void main(String[] args) { // 拿枚举类的对象做赋值 Season1 season1 = Season1.SPRING; System.out.println(season1.toString()); } } // 使用enum关键字定义枚举类 enum Season1 { //1. 提供当前枚举类的多个对象。多个对象之间用逗号隔开,末尾的对象分号结束 SPRING("春天", "春暖花开"), SUMMER("春天", "夏日炎炎"), AUTUMN("春天", "秋高气爽"), WINTER("春天", "冰天雪地"); //2. 声明Season对象的属性 private final String seasonName; private final String seasonDesc; // 3. 私有化类的构造器 private Season1(String seasonName, String seasonDesc) { this.seasonName = seasonName; this.seasonDesc = seasonDesc; } // 4. 其他诉求:获取枚举类对象的属性 public String getSeasonName() { return seasonName; } public String getSeasonDesc() { return seasonDesc; } }
用enum关键字定义枚举类,一上来就要声明好有限的、枚举类的对象。
注解(Annotation)
在用框架的时候,注解非常重要。
一定要知道注解不是注释!
注解是在jdk5.0增加的新特性
注解其实就是代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。
文档注释里也有注解。生成文档会有相关的注解
一定程度上,框架 = 注解 + 反射 + 设计模式
元注解:对现有的注解进行解释说明的注解
Annocation的使用示例
- 示例一:生成文档相关的注解
- 示例二:在编译时进行格式检查(JDK内置的三个基本注解) @Override: 限定重写父类方法, 该注解只能用于方法 @Deprecated: 用于表示所修饰的元素(类, 方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择 @SuppressWarnings: 抑制编译器警告
- 示例三:跟踪代码依赖性,实现替代配置文件功能
如何自定义注解:参照@SuppressWarnings定义
① 注解声明为:@interface
② 内部定义成员,通常使用value表示
③ 可以指定成员的默认值,使用default定义
④ 如果自定义注解没有成员,表明是一个标识作用。
如果注解有成员,在使用注解时,需要指明成员的值。
自定义注解必须配上注解的信息处理流程(使用反射)才有意义。
自定义注解通过都会指明两个元注解:Retention、Target
jdk 提供的4种元注解
元注解:对现有的注解进行解释说明的注解
Retention:指定所修饰的 Annotation 的生命周期:SOURCE\CLASS(默认行为)\RUNTIME
只有声明为RUNTIME生命周期的注解,才能通过反射获取。
Target:用于指定被修饰的 Annotation 能用于修饰哪些程序元素
出现的频率较低
Documented:表示所修饰的注解在被javadoc解析时,保留下来。
Inherited:被它修饰的 Annotation 将具有继承性。
十、集合
概述
一方面,面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。
另一方面,使用Array存储对象具有一些弊端,而Java集合就像一种容器,可以动态地把多个对象的引用放入容器中。
集合、数组都是对多个数据进行存储操作的结构,简称Java容器。
说明:此时的存储,主要指的时内存层面的存储,不涉及到持久化的存储(不涉及到硬盘层面)
数组在内存存储方面的特点:
- 数组初始化的时候必须指定长度,初始化之后,长度就固定了(不能对长度进行修改)。
- 数组一旦定义好以后,元素的类型就确定了(很严格),我们也就只能操作指定类型的数据了。比如:
String[] arr、int[] arr1
数组在存储多个数据方面的缺点:
- 一旦初始化以后,长度不可修改。(集合的长度是可以修改的!)
- 数组中提供的方法非常有限,对于添加、删除、插入数据等操作非常不便,而且是我们自己实现,没有现成的方法可以用,效率不高。数组这种结构只是数据结构中的一种,集合涉及到的数据结构就比较丰富了,像链表,做插入操作效率就比较高了。
- 获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用
- 数据存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。(数组体现在内存上,是一块连续的物理内存空间)
Java集合可分为Collection和Map两种体系。
这两个体系不是类,而是接口,是规范
集合框架
Collection
接口:单列数据,定义了存储一组对象的方法的集合List
接口:元素有序、可重复的集合 --> “动态”数组- 实现类:
ArrayList LinkedList Vector
- 实现类:
Set
接口:元素无序、不可重复- 实现类:
HashSet LinkedHashSet TreeSet
- 实现类:
List和Set也是接口,是Collection的子接口!
若想存储基本数据类型的元素,就用包装类去存。
Map
接口:双列数据。这里的map指映射。保存具有映射关系key-value对的集合。Map没有提供子接口- 实现类:
HashMap、 LinkedHashMap、 TreeMap、Hashtable、Properties
Collection接口中的方法的使用
contains(Object obj)
:判断当前集合中是否包含obj里面是用了equals(),如果重写了,那么比较的是内容,如果没有重写,比较的是地址值。
我们在判断时会调用obj对象所在类的equals()
向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals()方法。
containsAll(Collection coll1)
:判断形参coll1中的所有元素是否都存在于当前集合中。remove(Object obj)
: 从当前集合中移除obj元素removeAll(Collection coll1)
:从当前集合中移除coll1中所有元素retainAll(Collection coll1)
获取当前集合和coll1集合的交集,并直接修改当前集合为交集的结果(在原集合的基础上修改而不是给返回值但不修改原集合(String都是给返回值,而不修改原字符串,这是String的不可变性))equals(Object obj)
要想返回true,需要当前集合和形参集合的元素都相同。hashCode()
返回当前对象的哈希值toArray()
集合转换为数组Object[] arr = coll.toArray();
拓展:数组 --> 集合,调用Arrays类的静态方法,
asList()
iterator()
返回Iterator接口的实例,用于遍历集合元素Iterator接口是迭代器接口,作用就是用来遍历。迭代器也是设计模式的一种。
迭代器模式就是为容器而生,GOF给迭代器模式的定义为:提供一种方法访问一个容器对象中的各个元素,而又不需要暴露该对象的内部细节。
while (iterator.hasNext()) { System.out.println(iterator.next()); }
错误写法
while (iterator.next() != null) { System.out.println(iterator.next()); }
while (coll.iterator().hasNext()) { System.out.println(coll.iterator().next()); }
每当我们调iterator()方法,都会返回一个新的迭代器对象,默认游标都在集合的第一个元素之前。
迭代器对象内部定义了remove(),可以在遍历的时候,删除集合中的元素,此方法不同于集合直接调用remove()
iterator迭代器用来遍历Collection接口的实现类的对象,不遍历Map!
jdk5.0之后新增了一种集合的遍历方式,叫foreach,叫增强for循环,不光能遍历集合,数组也能遍历
for(集合中元素的类型 局部变量:集合对象){
}
内部仍然调用了迭代器,本质上仍然一样,形式上不一样。两种方式都可以用。
增强for循环(foreach)循环是重新赋了一个值,不会改变原有数组中的元素
String[] arr = new String[]{"MM", "MM", "MM"}; for (String s: arr) { s = "GG"; } // arr数组中的值仍然是"MM"
Collection接口的子接口--List接口
我们经常使用List替代数组,List看作是动态数组,长度是可变的,不用去关注长度够不够的问题,长度可以动态变化
List集合类中元素有序、且可重复。集合中的每个元素都有其对应的索引顺序
面试题:比较ArrayList、LinkedList、Vector三个实现类(List是接口)的异同
同:三个类都是实现了List接口,存储数据的特点相同:存储有序的、可重复的数据
异:
ArrayList
:作为List接口的主要实现类,线程不安全的,效率高;底层使用Object[]数组,说用ArrayList来替换数组,但是底层仍然是用数组实现!所以String的返回长度的方法length(),和list返回长度的方法size(),由于String底层是用不可变的字符数组,ArrayList底层是用Object[],他们的返回长度的方法最终都是数组的返回长度length。数据结构里,数组是典型的存储方式。LinkedList
:linked就是链表的意思,底层使用双向链表存储。对于频繁地插入、删除操作,使用此实现类效率比ArrayList高。对于链表来说,插入和删除操作,只是修改指针。Vector
:作为List接口的古老实现类,线程安全的,效率低,底层也是用Object数组实现
ArrayList的源码分析:
jdk7:
ArrayList list = new ArrayList()
// 底层创建了长度是10的Object[]数组elementDatalist.add(123);
// elementData[0] = new Integer(123);
list.add(11);
// 如果此次的添加导致底层elementData数组容量不够,则扩容,默认情况下,扩容为原来的容量的1.5倍。同时需要将原有数组中的数据复制到新的数组中。对于我们使用来说,感觉不到底层做了什么事,我们只管去add,但是底层涉及到扩容,类似于StringBuffer和StringBuilder,StringBuilder和ArrayList都是线程不安全的。
结论:建议开发中去使用带参的构造器:
ArrayList list = new ArrayList(int capacity);// capacity是自己指定的长度
jdk8: ArrayList的变化
- 第一次调add方法的时候才创建好数组,不像jdk7是在实例化对象的时候就在底层创建好了数组
ArrayList list = new ArrayList()
// 底层Object[] elementData初始化为{}
,并没有创建长度为10的数组,add的时候才创建list.add(123)
第一次调用add()时,底层才创建了长度为10的数组,并将数据123添加到elementData[0]。- 后续的添加和扩容操作与jdk7无异
小结:
jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例的懒汉式
延迟了数组的创建时间,节省了内存
LinkedList源码分析:
在数据结构中,涉及到底层存储,有两个基本的结构。
1.顺序表 2. 链表
在数据结构层面,ArrayList用的是顺序表的数组,LinkedList用的是链表。如果是频繁地插入删除,用LinkedList要好一些。LinkedList要维护所谓的指针。如果不涉及到插入删除,只是想遍历查找,那么就用ArrayList效率要高一些。
底层是用链表进行存储
LinkedList linkedList = new LinkedList();
Node是数据存储的基本单位
内部声明了Node类型的first和last属性,默认值为null
list.add(123)
将123封装到Node中,创建了Node对象其中Node定义为:
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; } }
通过这个结构可以看出LinkedList在数据结构层面体现为双向链表
不涉及到扩容
List接口中的常用方法:
Collection的子接口之二:Set接口
HashSet
:作为Set接口的主要实现类;线程不安全的;可以存储null值LinkedHashSet
:是HashSet的子类。在HashSet的基础之上,提供前后的指针,遍历其内部数据时,可以按照添加的顺序遍历(但是这并不代表有序)。TreeSet
:底层是从数据结构层面来说,是使用二叉树存的,使用红黑树存的。要求放入TreeSet的数据是同一个类new的对象,就可以按照这些对象的指定属性进行排序。Set:存储无序的、不可重复的数据
- 无序性:不等于随机性。遍历的时候每次的结果都是一样的,但是仍然是无序!无序性是相较于List。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。
- 不可重复性:保证添加的而元素按照equals()方法判断时,不能返回true(返回true,就认为两个对象是相同的,Object类中的equals()方法本来就是比较的地址值,在String类中被重写过,所以比较的是内容值,这里要看equals()方法比较的是什么值,并且不能返回true,不然就认为对象相同。),即相同的元素只能添加一个!
添加元素的过程:以HashSet为例:
HashSet底层在数据结构层面来说是以数组 + 链表的结构存储的!数组的初始长度是16,当使用率超过0.75,容量就会扩大为原来的两倍。
我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法去计算元素a的哈希值,此哈希值通过某种算法计算出在HashSet底层数组中的存放位置(即为索引位置),判断数组此位置上是否已经有元素,
如果此位置没有元素,则元素a添加成功 -----> 情况1
如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的哈希值,如果哈希值不相同,则元素a添加成功(情况2),如果哈希值相同,进而需要调用元素a所在类的equals()方法
- equals()方法返回true,元素a添加失败
- equals()方法返回false,元素a添加成功 -----> 情况3。
对于添加成功的情况2和情况3而言,元素a与已经存在指定索引位置上数据以链表的方式存储
jdk7:元素a放到数组中,指向原来的元素
jdk8:原来的元素在数组中,指向元素a
总结:七上八下
要求:
向Set中添加的数据,其所在的类一定要重写hashCode()和equals()两个方法。
重写的hashCode()和equals()尽可能保持一致性:两个相等的对象equals()方法,hashCode()方法的结果都要一样(相等的对象必须具有相等的散列码)
在开发中我们使用自动生成,能够保证以上的要求。
向HashSet中添加数据实际等于把数据添加到HashMap
HashSet底层实现是HashMap
LinkedHashSet的使用
LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据。 遍历其内部数据时,可以按照添加的顺序遍历(但是这并不代表有序)
优点:对于频繁的遍历操作,LinkedHashSet效率高于HashSet。
TreeSet
- 向TreeSet中添加的数据,要求是相同类的对象。因为只有是相同对象,我们才可以用指定属性来比较大小。
- 两种排序方式:自然排序和定制排序(TreeSet有序,不是说按添加顺序来排序,怎么排序需要我们自己去写逻辑)
- 自然排序中,比较两个对象是否相同的标准为:compareTo()返回0,不再是equals()(更严格一些。就是可能某一个属性一样了,就认为这两个对象是相同的了。)
- 定制排序中,比较两个对象是否相同的标准为:compare()返回0,不再是equals()
- TreeSet底层存储方式是红黑二叉树
Map接口
双列数据,存储key-value键值对数据
Map
HashMap
---作为Map的主要实现类,线程不安全,效率高;可以存储null的key和valueLinkedHashMap
保证在遍历map元素时,可以按照添加的顺序实现遍历,原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素对于频繁的遍历操作,此类执行效率高于HashMap
TreeMap
保证按照添加的key-value进行排序,实现排序遍历,此时根据key的自然排序或定制排序底层使用的红黑树!和TreeSet一样。
HashTable
----作为古老的实现类,线程安全,效率低,不能存储null的key和valueProperties
常用来处理配置文件,key和value都是String类型
HashMap的底层:
jdk7及以前:数组+链表
jdk8:数组+链表+红黑树
面试题:
HashMap的底层实现原理
Map结构的理解:
我们虽然说Map接口的实现类的实例化对象存储的是双列数据,但事实上Map当中放的还是一个一个的数据,叫做Entry,这个Entry当中有两个属性,一个属性是key,一个属性是value(Entry(k, v)),Entry是无序的且不可重复的,Map中的key是无序且不可重复的。
Map中的key是无序且不可重复的,使用Set存储所有的key -----> key所在的类要重写equals()和hashCode()
Map中的value,无序的,可重复的,使用Collection存储所有的value -----> value所在的类要重写equals()
一个键值对:key-value构成了一个Entry对象,是无序的,不可重复的,使用Set存储所有的Entry
熟悉这些,有助于我们写遍历操作
HashMap的底层实现原理 以jdk7为例说明
HashMap map = new HashMap();
在实例化以后,底层创建了长度是16的一维数组Entry[] table****可能已经执行过多次put...****
map.put(key1, value1);
首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法(映射算法)计算以后,得到在Entry数组中的存放位置。
如果此位置上的数据为空,此时的key1-value1这个Entry添加成功-----情况1
如果此位置上的数据不为空(意味着此位置上存在一个或多个数据(多个数据以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
- 如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1这个Entry添加成功-----情况2
- 如果key1的哈希值和已经存在的某一个数据的哈希值相同,那么继续比较,调用key1所在类的equals()方法:
- 如果equals()返回false:此时key1-value1添加成功------情况3
- 如果equals()返回true:使用value1替换相同key的value值。
补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储
在不断的添加过程中,会涉及到扩容问题,当超出临界值时(且要存放的位置非空)默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来
jdk8相较于jdk7在底层实现方面的不同:
new HashMap():底层没有创建一个长度为16的Entry数组
jdk 8 底层的数组是:Node[], 而非Entry[]
首次调用put() 方法时,底层创建长度为16的数组,类似于ArrayList
jdk7底层结构只有:数组 + 链表
jdk8中底层结构:数组 + 链表 + 红黑树
当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8且 当前数组的长度 > 64 ,此时此索引位置上的所有数据改为使用红黑树存储。目的是遍历快、方便查找
LinkedHashMap的底层实现原理
LinkedHashMap底层使用的结构与HashMap相同,因为LinkedHashMap继承于HashMap,区别就在于:LinkedHashMap内部提供了Entry(键值对),替换HashMap中的Node
怎么理解HashSet底层就是HashMap?
new一个HashSet,其实就new了一个HashMap,往HashSet中add元素的时候,实际上就是把元素放到了map中,这些元素被放到了HashMap中的key的位置,(因为HashSet底层就是HashMap,在jdk1.7及以前的底层是数组+链表,在jdk1.8是数组+链表+红黑树(要清楚什么时候会使用红黑树来进行存储(8 + 64))),向HashSet中添加元素,就是向HashMap的key位置添加元素,而HashMap的value位置,就放的是PRESENT常量,这个静态域中的PRESENT常量指向堆空间new的Object对象,是静态的,这就表示不会每add一次,就要在value位置新建一个Object对象,只有第一次add会在value位置新建Object对象,后面add元素,value都只是指向之前的Object对象,是为了不报空指针异常(因为HashSet相当于是只有HashMap的key,没有value)。
ArrayList
底层用顺序表的数组进行存储LinkedList
底层用双向链表进行存储HashSet
底层就是HashMap。 jdk7及以前:数组 + 链表。 jdk8:数组 + 链表 + 红黑树LinkedHashSet
提供前后的指针,遍历其内部数据时,可以按照添加的顺序遍历(但是这并不代表有序)。TreeSet
红黑二叉树HashMap
jdk7及以前:数组 + 链表。 jdk8:数组 + 链表 + 红黑树LinkedHashMap
添加了两个对象引用分别指向前一个元素和后一个元素TreeMap
红黑二叉树向TreeMap中添加key-value,要求key必须是由同一个类创建的对象,因为要按照key进行排序:自然排序、定制排序
自然排序就是实现Comparable接口,重写compareTo方法,定制排序就是在实例化TreeMap对象的时候,通过构造函数传入Comparator的对象,当然这个类要重写compare方法。
Comparable接口-----compareTo方法
Comparator接口------compare方法
Properties是Hashtable子类,常用来处理配置文件,key和value都是String类型
Collections工具类
Collections是一个操作Set、List和Map等集合的工具类,提供了一系列静态的方法。
ArrayList、HashSet、HashMap都是线程不安全的,但是我们也不会因为这个原因而去使用Vector或者Hashtable,所以要想办法把ArrayList、HashSet、HashMap变成线程安全的
// 返回的list1即为线程安全的list,那么就可以在多个线程中来操控这个list1了 List list1 = Collections.synchronizedList(List);
java数据结构
数据结构的研究重点是在计算机的程序设计领域中探讨如何在计算机中组织和存储数据并进行高效率的运用。
简单来说,数据结构就是一种程序设计优化的方法论,研究数据的逻辑结构和物理结构以及他们之间的相互关系。目的是加快程序的运行速度,减少内存占用的空间
算法是为了解决实际问题而设计的,数据结构是算法的载体,基于某种数据结构来写算法
数据结构的研究对象
- 数据见的逻辑结构
- 集合
- 线性关系 数据元素之间存在一个对一个的关系:顺序表、链表、队列、栈
- 树形结构 数据元素之间存在一个对多个的关系
- 网状结构(图状结构) 数据元素之间存在多个对多个的关系
- 数据的存储结构(物理结构)
- 数据见的逻辑结构
逻辑结构指的是数据间的关系,而存储结构(物理结构)是逻辑结构的存储映像。
通俗的讲,可以将存储结构理解为逻辑结构用计算机语言的实现。
物理结构是面向计算机的。
顺序表和链表是计算机中存在的真实结构
十一、泛型
泛型一定程度上可以理解为标签。指明容器里装的东西。规定了容器里装的东西。
数组只能存储同一类型的数据,这可以说是数组的好处,因为很严格,避免了一些漏洞,体现了强类型语言。
把元素的类型设计为参数,这个参数就叫泛型。
<E>
指的变量类型。这个泛型不能是基本数据类型,如果要存基本数据类型,我们需要用包装类。在集合中使用了泛型,在编译时就会进行类型检查,保证数据的安全。避免了强转操作,因为在使用泛型之前,集合里面的数据类型都是指明为Object,所以想用集合里的具体的数据的时候会涉及到强转,就容易出异常,通过泛型指定了数据类型,那么就保证了数据安全,同时也避免了后续可能出现的强转操作。
总结:
集合接口或集合类在jdk5.0时都修改为带泛型的结构
在实例化集合类时可以指明具体的泛型类型。
指明完以后,在集合类或接口中凡是定义类或接口时,内部结构使用到泛型的位置,都指定为实例化的泛型类型。
比如:add(E e) -----> 实例化以后:add(Integer e)
注意点:泛型的类型必须是类,不能是基本数据类型,需要用到基本数据类型的位置,拿包装类替换
如果实例化时,没有指明泛型的类型,默认类型为java.lang.Object类型
如何自定义泛型结构:泛型类、泛型接口;泛型方法。
泛型类和泛型接口其实没有多大的区别,本质上是类和接口的区别
public class Order<T> { String name; int orderId; //类的内部结构就可以使用类的泛型了。 T orderT; public Order() {} public Order(String name, int orderId, T orderT) { this.name = name; this.orderId = orderId; this.orderT = orderT; } public T getOrderT() { return orderT; } public void setOrderT(T orderT) { this.orderT = orderT; } }
/** * 1. 关于自定义泛型类 */ public class GenericTest { @Test public void test1() { // 如果定义了泛型类,然后实例化时,没有指明泛型的类型,那么认为此泛型类型为Object类型。 // 要求,如果定义了泛型类,那么在实例化类的对象时,要指明这个泛型是什么类型。 Order order = new Order(); order.setOrderT(123); order.setOrderT("abc"); // 定义了泛型类,在实例化类的对象时,要指明泛型的类型 Order<String> order1 = new Order<>("AA", 1001, "fufu"); order1.setOrderT("AA:hello"); } @Test public void test2() { SubOrder subOrder = new SubOrder(); // 由于,子类在继承带泛型的父类时,指明了父类的泛型的类型,则子类的实例化对象的时候,不再需要指明泛型类型。 subOrder.setOrderT(321321); } @Test public void test3() { SubOrder1<String> subOrder1 = new SubOrder1<>(); subOrder1.setOrderT("asdads"); } }
静态方法中不能使用类的泛型,泛型方法可以声明为静态的。
类的泛型是在实例化的时候指定的,而静态结构要早于对象的创建
泛型方法可以声明为静态,是因为泛型参数是在调用方法时确定的,而非实例化对象时确定的。
异常类不能是泛型的
泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系。换句话说,泛型方法所属的类是不是泛型类都没有关系
DAO:data(base) access object 数据访问对象
就理解成操作数据库的时候,提供的Java类,在这个类中提供操作数据库的操作
在DAO里面通俗地去定义一些操作数据库的方法
- 添加一条记录
- 删除一条记录
- 修改一条记录
- 查询一条记录
- 查询多条记录
每一张表都可能会涉及到这些操作,每一张表都对应于一个Java类,那么DAO里面就涉及到对许多个类的增删改查的操作。在DAO里,不知道是哪个类,就把DAO定义为泛型类,写通用的操作。
子类的实例化对象赋给父类的对象引用,就是多态的体现!
泛型在继承方面的体现
虽然类A是类B的父类,但是G<A> 和G<B>二者不具备子父类关系,二者是并列关系。A<G> 是 B<G>的父类
通配符的使用
通配符:
?
类A是类B的父类,但是G<A> 和G<B>二者不具备子父类关系,二者共同的父类是G<?>
对于List<?>就不能向其内部添加数据
除了添加null之外
十二、IO流
内存中的数据存到硬盘上(内存层面写入到持久化的层面),叫做输出
把持久化的层面(硬盘等)的数据读到内存中,叫做输入。
File类的使用
java.io.File
类:文件和文件目录路径的抽象表示形式,与平台无关怎么创建File类的对象
/** * 1. File类的一个对象,代表一个文件或一个文件目录(俗称:文件夹) * 2. File类声明在java.io包下 * */ public class FileTest { /* 1.如何创建File类的实例 2.相对路径:相较于某个路径下指明的路径 绝对路径:包含盘符在内的文件或文件目录的路径 3.路径分隔符:在windows下:\\ UNIX: / */ @Test public void test() { // 构造器1 File file1 = new File("./hello.txt"); // 相对于当前module File file2 = new File("D:\\installations\\projects\\ideaProjects\\shangguigu\\he.txt"); // 绝对路径 System.out.println(file1); System.out.println(file2); // 构造器2 File file3 = new File("D:\\installations\\projects\\ideaProjects", "shangguigu"); System.out.println(file3); // 构造器3 File file4 = new File(file3, "hi.txt"); System.out.println(file4); } }
如下的两个方法适用于文件目录:
public String[] list(); // 获取指定目录下的所有文件或者文件目录的名称数组 public File[] listFiles();// 获取指定目录下的所有文件或者文件目录的File数组
把文件重命名为指定的文件路径
// 要想保证返回true,需要file1在硬盘中是存在的,且file2不能在硬盘中存在 File file1 = new File("hello.txt"); File file2 = new File("D:\\io\\hi.txt"); boolean renameTo = file1.renameTo(file2);
File类中涉及到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法。
并未涉及到写入或读取文件内容的操作,如果需要读取或写入文件内容,必须使用IO流来完成。
后续File类的对象常会作为参数传递到流的构造器中,指明读取或写入的“终点”
IO流原理及流的分类
IO流概述
I/O是Input/Output的缩写,I/O技术用于处理设备之间的数据传输,网络之间的数据传输也可以叫I/O
Java程序中,对于数据的输入、输出操作以“流”的方式进行。
java.io包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据
输入input:读取外部数据到内存(程序)中。磁盘----->内存
输出output:将内存数据输出到磁盘。内存----->磁盘
输出叫写,输入叫读,我们要站在内存的角度。
非文本的数据用字节流,文本数据用字符流
流的分类:
- 操作数据单位:字节流、字符流
- 数据的流向:输入流、输出流
- 流的角色:节点流、处理流
流的体系结构
抽象基类 节点流(或文件流) 缓冲流(处理流的一种) InputStream FileInputStream BufferedInputStream OutputStream FileOutputStream BufferedOutputStream Reader FileReader BufferedReader Writer FileWriter BufferedWriter 缓冲流是作用在已有的流的基础之上
public class FileReaderWriterTest { /** * 将hello.txt文件读入到程序中,并输出到控制台 * 说明:1.read()的理解:返回读入的一个字符,如果达到文件末尾,返回-1 * 2. 异常的处理:为了保证流资源一定可以执行关闭操作,需要使用try-catch-finally处理 * 3.读入的文件一定要存在,否则就会报FileNotFoundException */ @Test public void test() { FileReader fileReader = null; try { //1.实例化File对象,指明要操作的文件 File file = new File("hello.txt"); // 相对路径 // 2.提供具体的流,file对象要作为参数传入流的构造器 fileReader = new FileReader(file); //3. 数据的读入 // read() 返回读入的一个字符,如果达到文件末尾,返回-1 int data = 0; while ((data = fileReader.read()) != -1) { System.out.println((char) data); } } catch (IOException e) { e.printStackTrace(); } finally { try { //4. 流的关闭操作 if (fileReader != null) { fileReader.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
read(char[] cbuf):
返回每次读入cbuf数组的字符的个数,如果达到文件末尾,返回-1输出操作(写操作):
/** * 说明: * 1. 输出操作(写操作):对应的File可以不存在,并不会报异常 * 2. 对应的File * 如果不存在,在输出的过程中,会自动创建此文件 * 如果存在,如果流使用的构造器是:FileWriter(file, false) / FileWriter(file):对原有文件的覆盖 * 如果流使用的构造器是:FileWriter(file, true) 不会对原有文件覆盖,而是在原有文件基础上追加内容 */ @Test public void testFileWriter() { FileWriter fileWriter = null; try { File file = new File("hello1.txt"); fileWriter = new FileWriter(file, true); fileWriter.write("fufushihuaidan\n"); fileWriter.write("ermeiwoaini\n"); } catch (IOException e) { e.printStackTrace(); } finally { try { fileWriter.close(); } catch (IOException e) { e.printStackTrace(); } } }
结论:
对于文本文件,使用字符流来处理
对于非文本文件,使用字节流来处理
处理流之一:缓冲流
缓冲流的作用就是提高文件的读写效率
能够提高读写速度的原因:内部提供了一个缓冲区
开发的时候不会用节点流。
处理流是作用在流上的。
处理流就是套接在已有的流(这个流不一定是节点流)的基础上。
处理流之二:转换流
作用:转换流提供了在字节流和字符流之间的转换
InputStreamReader
将一个字节的输入流转换为字符的输入流outputStreamWriter
将一个字符的输出流转换为字节的输出流解码:字节、字节数组 -----> 字符数组、字符串
编码:字符数组、字符串 -----> 字节、字节数组
标准输入流
说明:
String类型的变量是引用类型变量,那么这个变量的值不是null就是地址值,打印出来的是因为String的toString方法重写了,所以打印出来的是具体的内容的值。包括像直接打印其他一些引用类型的对象,都是有具体的值,也是由于重写了toString方法
而print和println方法有许多重载的方法,可以打印整型、字符串等,方法名相同而形参列表不同,形参列表不同指的是形参类型和形参顺序,这叫方法的重载。如下:
System.in
是标准的输入流,默认从键盘输入System.out
标准的输出流,默认从控制台输出打印流:PrintStream 和PrintWriter
- 提供了一系列重载的print()和println()方法
对象流
对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点
当其他程序获得了这种二进制流,还可以把它恢复成原来的Java对象
对象流的使用
- ObjectInputStream和ObjectOutputStream
- 作用:用于存储和读取基本数据类型数据或对象的处理流,它的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来
自定义类需要满足如下的要求,方可序列化:
- 需要实现接口:Serializable
- 当前类提供一个全局常量:serialVersionUID
- 除了当前Person类需要实现Serializable接口外,还必须保证其内部所有属性也必须是可序列化的(默认情况下,基本数据类型可序列化)
对象的序列化涉及到传输,但是在真正开发用的时候,经常把对象转换成json字符串去传输。String字符串是可序列化的。
十三、网络编程
网络编程概述
上一章是在本地实现的数据的传输,这一章讲的相当于是网络之间的IO
Java提供的网络类库,可以实现无痛的网络连接(就是不用关注底层实现,只需要调用API),联网的底层细节被隐藏在Java的本机安装系统里,由JVM进行控制,并且Java实现了一个跨平台的网络库,程序员面对的是一个统一的网络编程环境。
网络编程的目的
直接或间接地通过网络协议与其他计算机实现数据交换(数据传输),进行通讯
网络编程中有两个主要的问题:
- 如何准确地定位网络上一台或多台主机:定位主机上特定的应用
- 找到主机后如何可靠高效地进行数据传输
通信双方地址:
IP(网络当中唯一定位的主机)
端口号(定位这台主机上哪个应用程序进行通信)
一定的规则(网络通信协议):
- OSI参考模型:模型过于理想化,未能在因特网上进行广泛推广
- TCP/IP参考模型(或TCP/IP协议):事实上的国际标准
网络编程中的两个要素
- IP和端口号
- 对应问题二:提供网络通信协议,TCP/IP模型(物理+数据链路层、网络层、传输层、应用层)
IP和端口号
IP唯一地标识Internet上的计算机,是通信实体
本地回环地址hostAddress:127.0.0.1
hostName:localhost
IPV4:4个字节组成
IPV6:128位
在Java中使用
InetAddress
类代表IP域名:
www.baidu.com www.mi.com www.sina.com
DNS
域名解析服务器,将域名解析成IP,再用IP地址访问网络服务器。本地回路地址:127.0.0.1,表示本机的IP地址,对应着:localhost(可以看作域名)
如何实例化InetAddress
InetAddress ip = InetAddress.getByName("localhost"); System.out.println(ip); InetAddress localHost = InetAddress.getLocalHost(); System.out.println(localHost);
两个常用方法,getHostName() / getHostAddress()
端口号标识正在计算机上运行的进程
不同的进程有不同的端口号
端口分类:
端口号与IP地址的组合得出一个网络套接字:Socket
所以网络通信通常也叫Socket通信,网络编程也叫Socket编程
网络协议
通信协议,对速率、传输代码、代码结构、传输控制步骤、出错控制等指定标准
通信协议分层的思想:
同层间可以通信,上一层可以调用下一层,而与再下一层不发生关系,就是说不能隔层通信。
传输层协议中有两个非常重要的协议:
传输控制协议TCP
用户数据报协议UDP
TCP/IP模型的两个主要协议
传输控制协议TCP
网络互联协议IP
IP协议是网络层的主要协议,支持网间互联的数据通信
TCP/IP协议模型从更实用的角度出发,形成了高效的四层体系结构,即物理链路层、网络层、传输层、应用层
TCP UDP
TCP:
- 使用TCP协议前,必须建立TCP连接,形成传输数据通道
- 传输前,采用三次握手的方式,点对点通信,是可靠的额
- TCP协议进行通信的两个应用进程:客户端、服务端
- 在连接中可进行大数据量的传输
- 传输完毕,需释放已建立的连接、效率低
UDP:
- 将数据、源、目的封装成数据报,不需要建立连接
- 每个数据报的大小限制在64KB以内
- 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
- 可以广播发送
- 发送数据结束时,无需释放资源,开销小,速度快
UDP的特点是速度要快,丢一点数据(比如说几帧画面)也无所谓。播放视频
TCP四次挥手
客户端和服务端均可主动发起挥手操作,但是服务器一般都是一直运行,通常都是客户端主动断开连接,服务器不会主动断开连接,服务器是一直都在的。
不过从理论上来说,在网络编程中,任何一方执行close()操作即可产生挥手操作
TCP网络编程
public class TcpTest { @Test public void client() { Socket socket = null; OutputStream outputStream = null; FileInputStream fileInputStream = null; ByteArrayOutputStream baos = null; InputStream inputStream = null; try { InetAddress inetAddress = InetAddress.getByName("127.0.0.1"); socket = new Socket(inetAddress, 9090); outputStream = socket.getOutputStream(); fileInputStream = new FileInputStream(new File("111.jpg")); byte[] bytes = new byte[1024]; int len; while ((len = fileInputStream.read(bytes)) != -1) { outputStream.write(bytes, 0, len); outputStream.flush(); } socket.shutdownOutput(); // 接收来自于服务器端的数据,并显示到控制台上 inputStream = socket.getInputStream(); baos = new ByteArrayOutputStream(); byte[] bytes1 = new byte[20]; int len1; while ((len1 = inputStream.read(bytes1)) != -1) { baos.write(bytes1, 0, len1); } System.out.println(baos.toString()); } catch (IOException e) { e.printStackTrace(); } finally { if (fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (socket != null) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } if (baos != null) { try { baos.close(); } catch (IOException e) { e.printStackTrace(); } } if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Test public void server() { ServerSocket serverSocket = null; Socket socket = null; InputStream inputStream = null; FileOutputStream fileOutputStream = null; OutputStream outputStream = null; try { serverSocket = new ServerSocket(9090); socket = serverSocket.accept(); inputStream = socket.getInputStream(); fileOutputStream = new FileOutputStream(new File("444.jpg")); byte[] bytes = new byte[1024]; int len; while ((len = inputStream.read(bytes)) != -1) { fileOutputStream.write(bytes, 0, len); fileOutputStream.flush(); } // 服务器端给客户端反馈 outputStream = socket.getOutputStream(); outputStream.write("你好,美女,照片我已收到".getBytes()); } catch (IOException e) { e.printStackTrace(); } finally { if (fileOutputStream != null) { try { fileOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (socket != null) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } if (serverSocket != null) { try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
客户端
- 桌面版应用
- 浏览器(B/S架构)
服务端
- 自定义
- Tomcat服务器
UDP网络编程
- UDP数据报通过数据报套接字DatagramSocket发送和接收,系统不保证UDP一定能够安全送达目的地,也不能确定什么时候可以抵达,就是说UDP是不可靠的。
- UDP协议中每个数据报都包含了完整的地址信息,因此无须建立发送方和接收方的连接。如同发送快递包裹一样,目的地有没有人来接收并不确定,只管发送,地址信息都包含,并不会事先确定有人接收才发送
DatagramSocket
对象封装了UDP数据报,数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号
URL编程
写的网址就是URL,通过URL直接访问服务器上的资源。
地址就是URL,对应网络当中的一个资源!
通过URL定位服务器的资源
浏览器通过URL,访问服务器,请求数据,数据展示在页面上,浏览器就相当于是客户端,它和服务器之间的通信就需要遵循相应的协议(如HTTP),前端展示的知识就涉及到HTML、CSS、JavaScript。
HTTP是网络上传输HTML(超文本标记语言,标记语言和纯文本语言做区分)的协议,用于浏览器和服务器之间的通信。HTTP是超文本传输协议。
十四、反射
概述
动态语言:在运行时能改变程序内部结构的语言,解释型语言、脚本语言
静态语言:运行时结构就不可变了。Java其实是静态语言,但是通过反射机制也可以在运行期间才确定用什么类的对象,什么方法。所以Java具有动态的特性。
Java反射机制提供的功能
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时获取泛型信息
- 在运行时调用任意一个对象的成员变量和方法
- 在运行时处理注解
- 生成动态代理
反射的例子
public class ReflectionTest { // 反射之前,对于Person类的操作 @Test public void test1() { //1. 创建Person的实例化对象 Person p1 = new Person("Tom", 12); //2 通过对象调用其内部的属性和方法 p1.age = 10; System.out.println(p1); p1.show(); // 在Person类外部,不可以通过Person类的对象调用其私有结构 // 比如:name、showNation()、私有构造器,这是封装性的限制。 } // 使用反射,对于Person类的操作 @Test public void test2() throws Exception { Class clazz = Person.class; // 1.通过反射,创建Person类的对象 // getDeclaredConstructor() 参数:指明构造器的参数列表 // constructor.setAccessible(true); 保证此构造器是可访问的 Constructor constructor = clazz.getConstructor(String.class, int.class); constructor.setAccessible(true); Object obj = constructor.newInstance("Tom", 12); Person p = (Person) obj; System.out.println(p); // 2. 通过反射,调用对象指定的属性 // set() 参数1:指明设置哪个对象的属性, 参数2:将此属性值设置为多少 // get() 参数1:获取哪个对象的当前属性值 Field age = clazz.getDeclaredField("age"); age.set(p, 10); System.out.println(p); // 3. 通过反射,调用对象指定的方法 // getDeclaredMethod(): 参数1:指明获取方法的名称, 参数2:指明获取的方法的形参列表 // invoke():参数1:方法的调用者, 参数2:给方法形参赋值的实参 Method show = clazz.getDeclaredMethod("show"); show.invoke(p); System.out.println(); //4. 通过反射,可以调用Person类的私有结构,比如:私有的构造器、方法、属性 // 调用私有的构造器 Constructor declaredConstructor = clazz.getDeclaredConstructor(String.class); declaredConstructor.setAccessible(true); Object obj1 = declaredConstructor.newInstance("Jerry"); Person p1 = (Person) obj1; System.out.println(p1); // 调用私有的属性 Field name = clazz.getDeclaredField("name"); name.setAccessible(true); name.set(p1, "HANMEIMEI"); System.out.println(p1); // 调用私有方法 // getDeclaredMethod(): 参数1:指明获取方法的名称, 参数2:指明获取的方法的形参列表 // invoke():参数1:方法的调用者, 参数2:给方法形参赋值的实参 // 如果调用的运行时类中的方法没有返回值,则此invoke()方法返回null Method showNation = clazz.getDeclaredMethod("showNation", String.class); showNation.setAccessible(true); String s = (String) showNation.invoke(p1, "中国"); System.out.println(s); // } }
疑问:
通过直接new的方式或反射的方式都可以调用公共的结构,开发中到底用哪个?
建议:直接new的方式,但是也不排除用反射的方式
什么时候会使用反射的方式调用?
编译的时候不能确定我们要new哪个类的对象,这种时候就用反射的方式。运行时才能确定我们要造哪个类的对象,这种用反射。运行时来改变结构,这种是称为动态性。
反射机制与面向对象中的封装性是不是矛盾的?如何看待两个技术?
不矛盾。体现封装性的private关键字,就说明不让我们直接去调,如果一定要用反射的方式去调也是可以的,但是我们应当遵循封装性的原则。
关于java.lang.Class类的理解
类的加载过程:
程序在经过javac.exe命令(编译命令)以后,会生成一个或多个字节码文件(.class结尾)。
接着我们使用java.exe命令对某个字节码文件进行解释运行。相当于将某个字节码文件加载到内存中。此过程就称为类的加载。加载到内存中的类,我们就称为运行时类,此运行时类就作为Class类的一个实例对象。所以运行时类本身也是对象,这里也体现了万物皆对象。
换句话说,Class的实例就对应着一个运行时类。
加载到内存中的运行时类,会缓存一定的时间,在此时间之内,我们可以通过不同的方式来获取此运行时类,即Class的实例。
获取Class的实例的方式
// 获取Class的实例的方式 public void test3() throws ClassNotFoundException { // 1. 调用运行时类的属性 Class class1 = Person.class; //2. 通过运行时类的对象 Person p1 = new Person(); Class class2 = p1.getClass(); //3 调用Class的静态方法 Class class3 = Class.forName("com.atguigu.reflectTest.Person"); //4. 使用类的加载器:ClassLoader ClassLoader classLoader = ReflectionTest.class.getClassLoader(); Class class4 = classLoader.loadClass("com.atguigu.reflectTest.Person"); }
经常使用的是第三种
类的加载过程
创建运行时类的对象
只要是造对象,都得用构造器来造,只是表面上看来可能是调的某个方法,但是方法内部仍然是用的构造器!
创建运行时类的对象的方法:
@Test public void test1() throws InstantiationException, IllegalAccessException { Class<Person> clazz = Person.class; // newInstance() 内部调用了运行时类的空参构造器 /* 要想此方法正常地创建运行时类的对象,要求: 1.运行时类必须提供空参的构造器 2.空参的构造器的访问权限得够,通常设置为public 在javabean中要求提供一个public的空参构造器,原因: 1. 便于通过反射,创建运行时类的对象 2. 便于子类继承此运行时类时,默认调用super()时,保证父类有此构造器 */ Person o = clazz.newInstance(); System.out.println(o); }
通过反射获取属性
通过反射获取方法
通过反射还可以获取注解、异常、形参列表、返回值类型、构造器结构
通过反射获取构造器结构
调用运行时类的指定结构(指定属性、指定方法、指定构造器)(见反射的例子)
动态代理
例子
/** * 动态代理的举例 */ interface Human { public abstract String getBelief(); public abstract void eat(String food); } // 被代理类 class SuperMan implements Human { @Override public String getBelief() { return "I believe I can fly"; } @Override public void eat(String food) { System.out.println("我喜欢吃" + food); } } /* 要想实现动态代理,需要解决的问题: 1. 如何根据加载到内存中的被代理类,动态地创建一个代理类及其对象 2. 当通过代理类的对象调用这个方法时,如何动态地调用被代理类的方法 */ class ProxyFactory { // 调用此方法,返回一个代理类的对象。 public static Object getProxyInstance(Object obj) { // obj: 被代理类的对象 MyInvocationHandler myInvocationHandler = new MyInvocationHandler(); myInvocationHandler.bind(obj); return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), myInvocationHandler); } } class MyInvocationHandler implements InvocationHandler { private Object object; // 需要使用被代理类的对象进行赋值 public void bind(Object object) { this.object = object; } // 当我们通过代理类的对象,调用方法a时,就会自动调用如下的方法:invoke() // 将被代理类要执行的方法a的功能声明在下面的invoke()方法中 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //method:即为代理类对象调用的方法,此方法也就作为了被代理类要调用的方法。 Object returnValue = method.invoke(object, args); // 上述方法的返回值就作为invoke方法的返回值。 return returnValue; } } public class ProxyTest { public static void main(String[] args) { // superMan: 被代理类的对象 SuperMan superMan = new SuperMan(); // proxyInstance:代理类的对象 Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan); // 当通过代理类的对象调用方法时,会自动地调用被代理类中同名的方法 System.out.println(proxyInstance.getBelief()); proxyInstance.eat("四川麻辣烫"); System.out.println("************************"); } }
注意
StringBuffer、StringBuilder,底层char数组初始大小为16,如果底层需要扩容,是扩容为原来数组大小的2倍+2
ArrayList底层使用Object数组存储,初始大小为10,如果扩容是扩容为原来数组大小的1.5倍
LinkedList底层使用双向链表存储,不涉及到扩容
对于扩容来说,HashMap底层是Entry[]数组,数组的每一个位置是键值对链表,初始大小16,超过数组大小 * 加载因子,就会扩容为原来的2倍。
上面三点分别是char数组(jdk9开始就改为了byte数组),Object数组,Entry[]数组,可以这么来记,并且都有初始大小和扩容的倍数
HashMap底层在jdk7及以前是数组 + 链表形式存储
在jdk8及以后是数组 + 链表+ 红黑树形式存储
new了一个HashMap对象之后,在堆空间中会有一个初始大小为16的Entry[] table 数组,这个数组的每一个位置,是链表,所以是数组 + 链表的形式,是这么来理解。说到HashMap的大小,当然是说的最外层的Entry[]数组的大小,每一个位置是Entry的链表,也就是键值对的链表
当数组大小超过64,以及数组的某个位置上的元素以链表形式存储,且链表长度超过8,这时用红黑树存储
一直往HashMap里put,当超过某个阈值时,会扩容,扩容是扩容成原来的2倍!
这个阈值是Entry[]数组大小 * 加载因子。默认加载因子是0.75
比如Entry[]数组大小是16,那么超过12时,就要扩容了,扩容为原来的2倍
HashSet底层就是HashMap,是一样的。