计算机基本知识
一个小常识:java严格区分大小写,但是windows不区分,所以在命令提示符里大小写是一样的
命令行采用的字符集是GBK,识别中文的
程序=算法+数据结构
软件=程序+软件工程
-
冯· 诺伊曼体系结构
输入设备 ->储存器->输出设备
其中储存器要与cpu交换数据,cpu包含控制器和运算器
-
打开cmd 的方法:
-
开始->windows系统->命令提示符
-
windows+r 输入cmd
常见的Dos命令
#盘符切换 C: #查看当前目录下的所有文件 //dir #切换目录 //cd change directory cd\ //直接回到当前的根目录 cd /d 路径 //可以跨盘符切换 cd .. //进入上一级目录 cd 文件名 //进入下一级目录 #清理屏幕 //cls clean screen #退出终端 //exit #查看电脑ip //ipconfig #快速打开某些应用 //calc mspaint notepad #ping 命令 //得到一些网站的ip信息 eg : ping www.baidu.com #在当前目录创建文件夹(目录) //md 目录名 #在当前目录创建文件 //cd>文件名.后缀 #在当前目录创建文件且写入内容 //echo 内容>文件名.后缀 #删除文件 //del 文件名.后缀 #删除某一格式的所有文件 // del *.后缀 #删除文件夹 //rd 目录名
3.计算机硬件部分:
存储设备
持久性存储设备--硬盘
-
u盘
存储单位:比特(bit)字节(byte),比特最小,字节最基本。
内存:(RAM):由一个有序的字节序列组成,用于存储程序和程序需要的数据。
-
==一个程序和它的数据再被cpu执行前必须移到计算机的内存中==
-
数据:硬盘->内存->CPU
-
内存读取数据的速度比硬盘的存取速度快10倍,而CPU的速度比内存还不知快多少倍,CPU直接再内存运行数据,比CPU在硬盘运行程序要快很多。
-
数据由传输速度较慢的硬盘通过高速的内存通道传至CPU进行处理!
-
但内存是带电存储的(断电数据消失)而且容量有限,所以长时间存储设备只能放在硬盘中。
-
提升硬盘能明显提高数据传输速度。
-
内存中每个字节都有唯一对应的地址
输入和输出设备
常见的输入设备:键盘鼠标
常见的输出设备:显示器 打印机
显示器的屏幕分辨率:显示器水平和垂直方向上显示的像素数
-
分辨率越高,越细腻,清晰
-
屏幕尺寸:屏幕对角线的长度。
-
像素密度:对角线上像素的个数/屏幕尺寸。
通信设备:电话线->网卡->无线网
4.计算机的鼻祖:
冯 · 诺伊曼、图灵
5.操作系统
-
操作系统(operating system)是运行在计算机上的最重要的程序,它可以管理和控制计算机的活动。
6.万维网:(world wide web,www环球信息网),常称为web。
-
www可以让web客户端(浏览器)访问web服务器上的页面
-
每个资源由一个全局“统一资源标识符“(URL)标识,这些资源通过超文本传输协议(http协议)传送给用户
-
万维网、因特网、互联网的关系:互联网包含因特网,因特网再包含万维网
-
软件架构:B/S架构(browser server):通过浏览器访问
-
软件架构:C/S架构(client server):例如qq等不需要经过浏览器
7.有效的学习方法:
-
学习的捷径:狂敲代码、花30分钟整理、要培养好的编码习惯
第一章 JAVA语言概述
一、软件开发介绍
1.软件开发:
软件:即一系列按照特定顺序组织的计算机数据和指令的集合,有系统软件和应用软件之分。常见的操作系统:windows,linux,mac,Android、ios
2。人机交互方式:
-
图形化界面:简单直观
-
命令行方式:需要控制台,输入特殊指令。
3.常用的dos命令
在最上方
二、编程语言介绍
第一代语言:机器语言
第二代语言:汇编语言
第三代语言:高级语言
-
面向过程:c、pascal、fortran (c开发效率低,但是执行效率高,常用于底层)
-
面向对象:java纯面向对象、cpp面向过程/面向对象
-
.net 跨语言的平台
-
py、scala (python开发效率高,但执行效率低)(java的效率介于py和c之间)
三、java语言概述
java富有吸引力是因为java程序可以在web浏览器中运行。
java技术在web方面的不断成熟。已经成为web应用程序的首选开发语言
jdk(java development kit):java开发工具包
ide(integrated develop environment):集成开发环境
java se (java standard edition):java标准版
java ee(java enterprise edition ):java企业版
java me (java micro edition):java小型版
java的应用:
-
企业级应用
-
android 应用
-
大数据平台开发
四、java语言运行机制和运行过程
java可以看成是类c语言发展和衍生的产物,是一个纯粹的面向对象的语言;舍弃了c语言中的指针,增加了垃圾回收的功能。
java语言的特点:
-
面向对象 :(1)两个基本概念:类、对象
(2)三大特性:封装、继承、多态
-
健壮性:吸收c语言的优点、舍弃指针
-
跨平台性:通过java语言编写的程序在不同的系统平台上都可以运行。
-
原理:只要在需要运行java应用程序的操作系统上,先安装一个java虚拟机(JVM)即可,由JVM来负责java程序在该系统中的运行,针对不同的操作系统,JVM也是不同的
-
java的两种核心机制:1.java虚拟机(java virtal machine)
2.垃圾收集机制(garbage collection)
java程序还会出现内存泄漏和内存溢出的问题吗?是的,会出现。
什么是JDK,JRE
-
JDK:java Development kit;java开发工具包。
-
JDK是提供给java开发人员使用的,其中包含了java的开发工具,也包括了JRE,所以安装了JDK,就不用再单独安装JRE了,
-
开发工具:编译工具(javac.exe),打包工具(jar.exe)。
-
JRE:Java Runtime Environment ; Java运行环境
-
包括java虚拟机(JVM)和java程序所需的核心类库等,如果想要运行一个开发好的java程序,计算机中只需要安装JRE即可。
-
简而言之,使用JDK的开发工具完成的java程序,交给JRE去运行。
JDK、JRE、JVM关系
API:(Application Programming interface):应用程序接口
五、java语言的环境搭建
下载jdk步骤以及搭建环境:orcale官网下载->打开安装包安装->cmd命令安装jre->配置环境变量(系统高级设置->环境变量->新建JAVA_HOME,路径选择jdk所在,在path变量(path的作用是指定命令搜索路径)里添加两行命令)
path环境变量:windows操作系统执行命令时所要搜索的路径
-
配置环境变量的目的,让系统在任何路径下都能识别java和javac命令
六、开发体验---Helloworld
搭建环境之后,我们可以在记事本编写一段java代码到扩展名为.java的文件,然后在java环境下运行(通过javac命令对java文件编译、再通过java命令对生成的class文件进行运行)
七、注释(Comment)
-
用于注解说明解释程序的文字就是注释
-
Java中的注释类型:
-
单行注释
-
多行注释
-
文档注释(Java特有)
-
注释的作用:
-
对所写的程序进行解释说明,增强可读性,方便everyone
-
调试所写的代码(如果报错,试着注释掉一些,找出错因)
-
注释的特点:单行注释和多行注释,注释了的内容不参与编译,换句话说,编译后生成的,class结尾的字节码文件中不包含注释掉的信息
-
==文档注释(Java特有)==
-
格式:
/** @author 指定Java程序的作者 @version 指定源文件的版本 */
-
文档注释的使用:
-
注释内容可以被JDK提供的工具javadoc所解析,生成一套以网页文件形式体现的该程序的说明文档
-
操作方式:先将源文件编译,然后用javadoc工具解析,具体如下:
javadoc -d 名字 -author -version 类名.java
-
-
多行注释的一个注意点:
-
多行注释不能嵌套多行注释
-
-
使用文档注释来注释整个类或整个方法
如果注释方法中的某一个步骤,使用单行或多行注释
八、java API文档
-
API(Application Programming Interface,应用程序编程接口),是java提供的基本编程接口(类库)
-
Java语言提供了大量的基础类,因此Oracle也为这些基础类提供了相应的API文档,用于告诉开发者如何使用这些类,以及这些类里包含的方法。
-
API就像是个字典一样,查询各种类别
-
API我在edge书签上放了matools的在线翻译版,以及电脑里java目录里有官方英文版
-
API实际上也是用文档注释写出来的
-
包->类->具体说明
九、对第一个java程序的总结
-
java程序编写->编译->运行的过程
-
编写:我们将编写的java代码保存在以”.java“结尾的源文件中
-
编译:使用java.exe命令编译我们的java源文件,格式:
-
java 源文件名.java
-
-
运行:使用java.exe命令解释运行我们的字节码文件。格式:
-
java 类名
-
-
在一个java源文件中可以声明多个class。但是,只能最多有一个类声明为public的。而且要求声明为public的类的类名必须与源文件名相同。
-
程序的入口是main()方法。格式是固定的。
public static void main(String[] args){ }
其中args是argument的缩写,表示参数,可由别的标识符代替;
还有中括号的位置可以放在参数的后面,
其他的都不能变了
-
输出语句:(两种)
-
System.out.println(); //先输出数据,输出完换行
-
System.out.print(); //只输出数据
-
-
若System.out.println(); 括号里没有东西,就表示换行
-
每一行执行语句都以”;“结束
-
编译的过程:编译以后,会生成一个或多个字节码文件,字节码文件的文件名与java源文件中的类名相同
-
只有包含main方法的类可以得到运行,如果程序中包含多个类,每个类中都有main函数,你要运行哪个main方法就把鼠标放在哪,如果直接运行,会从上到下
-
在使用javac编译时,如果程序中有多个类,就会生成对应数量的字节码文件,如果要运行哪一个类,就用java命令运行对应的字节码文件。Java 字节码文件名
-
编译时报错通常是语法发生错误;运行时报错可能是跑不动了(计算时的错误)
十、java集成开发工具(IDE)
-
JBuider
-
NetBeans
-
Eclipse
-
myeclipse
-
idea(Intellij IDEA)
第二章、基本语法
一、关键字与保留字
-
关键字(keyword)的定义和特点
-
定义:有特殊含义,用作专门用途的字符串(单词)
-
特点:关键字中所有字母都小写
-
-
保留字:现有java版本尚未使用,但是以后版本可能会作为关键字使用。字节命名标识符要避免使用这些保留字。(goto、const)
二、标识符(identifier)
-
标识符:
-
java对各种变量和方法和类等要素命名时使用的字符序列称为标识符
-
凡是自己取名字的都叫标识符
-
-
定义合法标识符的规则:
-
由26个英文字母大小写,0-9,_或者$组成
-
数字不可以开头
-
不可以使用关键字和保留字,但是能包含它们
-
java中严格区分大小写,但是不限制长度
-
标识符不能包含空格
-
-
java中的名称命名规范
-
包名:多单词组成时所有字母都小写
-
类名、接口名:多单词组成时,所有单词的首字母大写:XxxYy
-
变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写:xxxYyyZzz
-
常量名:所有字母都大写。多单词时每个单词都用下划线连接:XXX_YYY_ZZZ
-
在起名字时尽量要有意义
-
java采用unicode字符集,因此标识符也可以用汉字声明,但是不推荐
-
不遵循规范不会报错,但是推荐大家遵守规范
-
三、变量
1.变量的使用
-
变量的概念:
-
内存中的一个存储区域
-
该区域的数据可以在同一类型范围内不断变化
-
变量是程序中最基本的存储单元,包含变量类型、变量名和存储的值
-
-
java定义变量的格式:数据类型 变量名 = 值
-
说明:
-
未声明或==未赋值==前都不能使用变量
-
变量都定义在其作用域内,作用域就是它定义所在的一对大括号{}里面,在作用域内,它是有效的,出了作用域,就无效了
-
同一个作用域内,不能声明两个同名的变量
-
使用变量名来访问这块区域的值
-
-
变量的作用:用于在内存中保存数据
2.java变量的分类(按数据类型分)
-
对应每一种数据都定义了明确的具体数据类型(强类型语言),在内存中分配了不同大小的内存空间
-
基本数据类型:
-
整型:byte、short、int、long
-
浮点型:float、double
-
字符型:char
-
布尔型:boolean
-
-
引用数据类型:
-
类(class)
-
接口(interface)
-
数组(array)
-
-
==引用数据类型只可能存储两类值:null和地址值==
还可以按照变量在类中声明的位置来分:成员变量、局部变量
具体的数据类型:
3.整型:
-
byte(一字节=8bit)short(2字节)int(4字节)long(8字节)
-
byte的范围:-128~127
-
java各整数类型有固定的表数范围和字段长度,不受具体os的影响,以保证java程序的可移植性
-
java的整型常量默认为int型,==声明long型变量,必须要以“l”或“L”结尾==
-
通常定义整型变量时,使用int型,只有数特别大时,才有long
4.浮点型
-
与整数类型类似,浮点型也有固定的表数范围和字段长度,不受具体os的影响
-
浮点型常量有两种表示形式:
-
十进制形式:如6.5、54.765(必须要有小数点)
-
科学计数法形式,如5.23e34、56E-32
-
-
float:单精度尾数可以精确到7位有效数字,很多情况下,精度很难满足需求
-
double:双精度,精度是float的两倍,常用此类型
-
float表示的数值的范围比long还大
-
==定义float类型变量时,表里要以”f“或”F结尾“==
-
浮点型常用double
5.字符类型
-
char型数据用来表示通常意义上”字符“
-
char(==1字符=2字节==)
-
java中的所有字符都使用unicode编码,故一个字符可以存储一个字母,一个汉字,或者其他书面语的一个字符
-
字符串变量的三种表现形式:
-
字符常量是用单引号 ' ' 括起来的==单个==字符。例如 char c1=’a‘;char c2=’中‘
-
java中还允许使用转义字符 ’ \ ‘ 来将其后的字符转变为特殊字符型常量。例如’\n ‘,值得注意的是,如何取消\n的转义含义?在\n前面再加个\,如果要输出双引号,也是用\将其转义
-
直接使用unicode值来表示字符型常量:’\uXXX‘。其中,XXX代表一个十六进制整数,如’\u000a‘表示\n
-
-
char型单引号里面必须放一个字符,没有字符会报错
-
注:还可以用ascii码定义:char a1=97;表示‘a’赋值给a1(很少用)
-
char类型是可以进行运算的,因为它都对应有unicode码,转义字符也有对应的ascii码
小知识: ASCII码是美国人发明的,只用128个数字对应128个符号 但这128个数字只能表示英文字母和一些简单的符号,其他国家的文字怎么表示?这就需要一个容量更大的编码集 名词解释:乱码:世界上有很多编码方式,同一个二进制数字可以被解释成不同的符号,因此,想要打开一个文本文件,就必须知道它的编码方式,否则用错误的方式解读,就会出现乱码。 unicode:一种编码,将世界上所有的符号都纳入其中,每一个符号都给予一个独一无二的编码,使用unicode没有乱码的问题 unicode缺点:unicode只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储,无法区别unicode和ascii,三个字节表示一个符号还是三个符号?另外,英文字母只用一个字节就够表示了,但是unicode的内存是固定的2个字节或者其他,浪费了很多存储空间 从而出现了UTF-8 UTF-8是在互联网上使用最广的一种unicode的实现方式 UTF-8是一种变长的编码方式,它可以使用1-6个字节表示一个符号,根据不同的符号而变化字节长度。 UTF-8的编码规则: 1.对于单字节的UTF-8编码,该字节的最高位是0,其余7位用来对字符进行编码(等同于ASCII码) 2.对于多字节的UTF-8编码,如果编码包含n个字节,那么第一个字节的前n位为1,第一个字节的第n+1位为0,该字节的剩余各位用来对字符进行编码。在第一个字节之后的所有的字节,都是最高两位为“10”,其余6位对字符进行编码。
6.布尔类型(boolean)
-
只能取两个值之一:true 、false
-
跟c语言不同,c语言是0表示false,非0的数表示true,而java中就直接用true和 false
-
常常在条件判断、循环结构中使用
7.基本数据类型转换
boolean类型不能与其它数据类型运算。
(1)自动类型转换(自动类型提升):容量小的类型自动转换为容量大的数据类型,数据类型按容量大小排序为:
-
有多种类型的数据混合运算时,系统首先自动将所有数据转换成容量最大(不是所占字节最大)的那种数据类型,然后再进行计算。
-
说明:此时的容量大小指的是表示数的范围的大和小。
-
-
byte、short、char之间不会相互转换,它们三者在计算时首先转换为int类型(byte与byte做运算结果也是int)
-
当把任何基本数据类型的值和字符串(String)进行连接运算时(+),基本数据类型的值将自动转化成字符串(String)类型。
-
Java中对数据类型的要求比c语言更加严格,c语言中允许将char与short类型的和赋值给short型变量,但是java中不能将byte与short的和赋值给short型变量(它们都自动转化成int型,只是java更严格,不能赋给short)
(2)强制类型转换:自动类型提升的逆运算
-
将容量大的数据类型转换为容量小的数据类型。使用时要加上强制转换符:(),但可能造成精度降低或溢出,要注意。
-
通常,字符串不能直接转换为基本类型,但通过基本类型对应的包装类则可以实现把字符串转换成基本类型
-
==boolean类型不可以转换为其他的数据类型。==
一些例子:
-
long l1=3212;//定义long型没有加L或l,实际上是把3212看作int型再赋给一个long型的变量,一般没问题,但如果是一个超过int范围的数,就会报错
-
float f1=12.3;//定义float型没有加f或F会报错,因为12.3默认是double型,==把范围大的赋给范围小的需要用强制类型转换==,而这里没有,就报错。
-
对于整型常量,默认类型为int型,浮点型常量,默认类型为double型。
-
在java中,把范围大的类型转换成范围小的,只能用强制类型转换,而c语言中可以直接赋值,说明java对数据类型的要求更加严格
8.字符串类型:String
-
String不是基本数据类型,属于引用数据类型,翻译为:字符串
-
使用方法与基本数据类型一致。例如:String str=“abc”
-
String类型可以放多个、单个字符或者只有一对双引号,而char类型不能只有一对单引号
-
String可以和8种基本数据类型变量做运算(包括boolean类型),且运算只能是连接运算,+,一个字符串可以串接另一个字符串,也可以直接串接其他类型的数据。如:
-
str=str+“xyz”
-
又如:
int n = 100; str= “xyz”+n; //结果是xyz100
-
-
连接运算的结果仍然是String类型
-
字符类型(char)与数运算的结果是数,字符串类型(String)与数运算的结果是字符串
-
String str=“xiy”+‘x’; 结果是xiyx,说明字符串和字符之间的运算也是连接 char a=‘a’; int num = 10; String str = "xyz"; System.out.println(a+num+str); 输出结果是107xyz
==字符串类型不能转换成int或其他类型==
如 : int a = (int ) "abc" ; 编译会出现错误
注意:自动类型提升和强制类型转换只能是基本数据类型之间
9.进制与进制间的转换
-
所有数字在计算机底层都以二进制形式存在
-
整数有四种表示方式:
-
二进制,以0b或0B开头
-
八进制,以0开头
-
十进制
-
十六进制,以0x或0X开头
-
-
print输出的时候都是展现出十进制的
-
关于原码、反码、补码的概念:第一位是符号位,在计算反码、补码的过程中不计入,==计算机底层存储的都是补码==
-
由二进制求十进制:给的都是补码,要减一,取反得到原码
-
十进制求二进制:除2逆向取余
四、运算符
算术运算符、赋值运算符、比较运算符(关系运算符)、逻辑运算符、位运算符、三元运算符
1.算术运算符
-
取模运算要注意结果的符号与被模数一致
-
java中浮点数可以进行取模运算,这是c语言中不支持的
-
除法运算12/5=2,int/int=int
double a = 12 / 5 ; a的结果是2.0 ,因为12/5是2,2转换成double型是2.0
-
有以下的代码:要将a的值加一
short a= 1; a=a+1;//编译时会报错,因为a+1是int型,大的不能自动转换成小的 a++;//可以,因为自增自减不会改变数据类型
2.赋值运算符(=)
-
当“=”两侧数据类型不一致时,可以使用自动类型转换或强制类型转换原则进行处理
-
支持连续赋值。
有以下的复合运算符:
+=、-=、*=、/=、%=
-
复合运算符也不改变数据类型:
short a = 1; a=a+1 ; //编译失败,因为a+1是int型的 a+=1; //编译成功,因为+=不改变数据类型
-
因此对某个数据进行自身变动数值,建议用复合赋值和自增自减
3.比较运算符(关系运算符)
==、!=、>、<、>=、<=、instanceof
比较运算符的结果是boolean类型,要区分==和=
-
== 和 !=不仅可以使用在数值类型的数据之间,还可以使用在其他引用数据变量之间。
-
=== =永远是用来比较内存中的地址的,基本类型感觉上是在比较内容,实际上还是在比较地址!==基本类型都是存在栈里面的,Int a=3;当你int b=3的时候,jvm会干嘛呢?首先,它会去栈里面找是不是已经有一个数据为3了。如果不是的话新建一个地址,是的话,就把b指向这个地址, 所以对于基本类型来说,当值相同的时候,地址也是相同的,所以当你对基本类型的时候,内容相同的肯定返回的是true。。因为实质上,他们的地址也是相同的
-
因此对于基本数据类型来说,可以用==来判断二者是否相等,而对于引用数据类型(例如字符串),要用a.equals(b)来判断a和b是否相等。
4.逻辑运算符
& 逻辑与 | |逻辑或 | !逻辑非 |
---|---|---|
&&短路与 | ||短路或 | ^逻辑异或 |
a^b为真当且仅当a与b的值不相同
逻辑运算只针对布尔类型
区分&与&&:
-
相同点1:&与&&的运算结果相同
-
相同点2:当符号左边的true时,二者都会执行右边的运算
-
不同点:当符号左边是false时,&继续执行符号右边的运算,&&不再执行符号右边yyy的运算。(相当于短路)
区分|与||:
-
相同点1:|与||的运算结果相同
-
相同点2:当符号左边的false时,二者都会执行右边的运算
-
不同点:当符号左边是true时,|继续执行符号右边的运算,||不再执行符号右边的运算。(相当于短路)
开发中推荐使用&&和||,因为能省事
5.位运算符
-
位运算是直接对==整数==的二进制的运算
-
<<:在一定范围内,每向左移一位,相当于*2;超过限度后,首位的1可能变成符号位,进而发生突变
-
`>>:在一定范围内,每向右移一位,相当于/2;
最高效地计算2*8,8<<1;
位运算都是针对计算机底层二进制数的运算
~运算是可逆的,即 ~~a=a
6.三元运算符
格式:(条件表达式)?表达式1:表达式2
-
如果表达式为true,执行表达式1
-
如果表达式为false,执行表达式2
-
没有要求表达式1和表达式2结果是相同类型的,但是要能统一,方便赋值
-
三元运算符是可以嵌套使用的
-
凡是用到三元运算符的地方,都能改写成if-else语句,反之不成立
-
如果程序既可以使用三元运算符,又可以使用if-else结构,那么优先选择三元运算符,原因:简洁、执行效率高
7.运算符的优先级
五、程序流程控制
三种基本流程结构:顺序结构、分支结构、循环结构
1.分支结构:if-else 、switch-case
-
if语句三种格式:
-
==else结构是可选的==
-
针对条件表达式,如果多个条件表达式之间是“互斥”关系,哪个判断和执行语句声明在上面还是下面,都可以;如果多个条件表达式之间不是“互斥”关系,需要根据实际情况,考虑清楚应该将哪个结构声明在上面;如果多个条件表达式之间是“包含”关系,通常情况下要将范围小的放在上面,否则范围小的无法执行。
-
可以嵌套
-
执行语句只有一条时可以省略大括号,但是不推荐
如何从键盘获取不同类型的变量?需要使用Scanner类
具体步骤:
1.导包:import java.util.Scanner;
2.Scanner的实例化:Scanner scan = new Scanner (System.in);
3.调用Scanner 类的相关方法,来获取指定类型的变量
import java.util.Scanner; class ScannerTest{ Public Static void main(String[] args){ Scanner scan = new Scanner(System.in) int num = scan.nextInt(); } }
Scanner类的相关方法能在api文档上查阅,已知的是
-
next()输入字符串
-
nextXxx()输入某类型
注意:需要根据相应的方法来输入指定类型的值,如果输入的数据类型与要求的类型不匹配时,会报异常:InputMisMatchException导致程序终止。
对于char型的获取,Scanner没有提供相应的方法,只能获取一个字符串。然后获取索引为x位置上的字符(用charAt方法)。
String gender = scan.next(); char genderChar = gender.charAt(0)
如何获取一个随机数?
用Math类提供的方法random
Math.random()可以获得一个[0.0,1.0)之间的随机浮点数
如何获得闭区间内的随机整数呢?假设要获得[a,b]
有公式:[a,b]:(int)(Math.random* (b-a+1))+a
例如:[10,99]:
(int)(Math.random* (b-a+1))+a Math.random(); //[0.0,1.0) Math.random()*(89+1); //[0.0,90.0) (int)(Math.random* (89+1)) //[0,89] (int)(Math.random* (89+1))+10//[10,99]
2.分支语句2:switch-case结构
-
根据switch表达式中的值,依次匹配各个case中的常量,一旦匹配成功,则进入相应case结构中,调用其执行语句,当调用完执行语句后,则仍然继续向下执行其他case结构中的执行·语句,直到遇到break关键字或此switch-case结构末尾结束为止。
-
break可以使用在switch-case结构中,表示一旦执行到该关键字,就跳出switch-case结构。
-
switch结构中的表达式只能是如下的6种数据类型之一:byte、short、char、int、枚举类型(JDK5.0新增)、String类型(JDK7.0新增)。
-
case之后只能声明常量,不能声明范围
-
break是可选的
-
default也是可选的,default相当于if-else中的else,default位置是灵活的。
-
如果switch-case结构中的多个case的执行语句相同,可任意考虑合并
注:1.凡是可以使用switch-case的结构,都可以转换成if-else,反之不成立
2.当分支结构既可以使用switch-case(case后面表达式的取值情况不能太多),又可以使用if-else时,优先使用switch-case,因为它执行效率稍高
3.循环结构(for循环)
java中有for循环、while循环、do-while循环
(1)循环结构的四要素:
-
初始化条件 //1
-
循环条件 (布尔类型) //2
-
循环体 //3
-
迭代条件 //4
(2)for循环的结构
for (1;2;4){ 3; }
1->2->3->4->2->3->4......->2(不满足)
-
循环里的循环变量是局部变量,跳出循环结构后消失,不能再使用
一旦在循环中执行到break,就跳出循环
(3)while循环的结构
1; while(2){ 3; 4; }
-
while循环小心不要丢了迭代条件,丢了会出现死循环,这是绝对不允许的
-
for 循环和while循环是可以相互转换的~
-
==for循环和while循环体内定义的变量,跳出循环后都不得再使用==
(4)do-while循环的结构
1 do{ 3; 4; }while(2);
do-while循环至少会执行一次循环体
开发中,使用for和while更多一些,较少使用do-while
-
while(true)和for(; ;)一样,表示循环不受循环条件的影响而终止
-
结束循环的几种方式:
-
方式1:循环条件部分返回false
-
方式二:再循环体中,执行了break
-
(5)嵌套循环:
-
将一个循环结构A声明在另一个循环结构B的循环体中,就构成了嵌套循环
-
外层循环和内层循环
-
内层循环结构遍历一遍,只相当于外层循环体执行了一次
-
假设外层循环执行m次,内层循环要执行n次,则内层循环体实际上需要共执行n*m次
求程序运行花费时间的方法:
第一步:用System类里面的currentTimeMillis()方法求出当前距离1970-01-01 00:00:00 时刻的毫秒数
第二步:在程序的开头和结尾都使用这个方法,然后计算二者之差,就是程序运行所需花费的时间
求100内素数程序:
class OutputPrime { public static void main(String[] args) { int count = 0; for (int i = 2; i <= 100 ; i++ ) { int isPrime = 1; for (int j = 2 ; j < i ; j++ ) { if (i % j == 0) { isPrime = 0; } } if(isPrime == 1) { System.out.print(i+"\t"); count++; } if (count % 5 == 0) { System.out.println(); } } } }
优化1:加入break
class OutputPrime { public static void main(String[] args) { int count = 0; for (int i = 2; i <= 100 ; i++ ){ int isPrime = 1; for (int j = 2 ; j < i ; j++ ){ if (i % j == 0){ isPrime = 0; break;//break在这里加入,只对本身是非质数的自然数是有效的 } } if(isPrime == 1) { System.out.print(i+"\t"); count++; } if (count % 5 == 0){ System.out.println(); } } } }
经过运行发现求100k以内的素数,通过break可以减少到原来花费时间的1/10
优化二:改变内部循环的循环条件(大幅改进运行速度)
class OutputPrime { public static void main(String[] args) { int count = 0; for (int i = 2; i <= 100 ; i++ ) { int isPrime = 1; for (int j = 2 ; j <= Math.sqrt(i) ; j++ )//优化二在这里。只对本身是质数的数有影响 { if (i % j == 0) { isPrime = 0; break; } } if(isPrime == 1) { System.out.print(i+"\t"); count++; } if (count % 5 == 0) { System.out.println(); } } } }
(6)特殊关键字的使用:break、continue
使用范围 | 在循环中使用的作用 | 相同点 | |
---|---|---|---|
break | switch-case结构、循环结构中 | 结束当前循环 | 关键字后面不能声明执行语句 |
continue | 循环结构中 | 结束当次循环 | 关键字后面不能声明执行语句 |
break和continue的相同点补充:若在关键字后面声明了执行语句,编译时就会报错(十分严格)
==带标签的break和continue的使用:==
质数的实现方式二:用带标签的continue语句
class OutputPrime { public static void main(String[] args) { int count = 0; label:for (int i = 2; i <= 100 ; i++ ) { for (int j = 2 ; j <= Math.sqrt(i) ; j++ ){//优化二在这里。只对本身是质数的数有影响 if (i % j == 0){ continue label; } } System.out.print(i+"\t"); count++; if (count % 5 == 0) { System.out.println(); } } } }
(7)return的使用(结束方法)
-
return并非专门用于结束循环,它的功能是结束一个方法。当一个方法执行到一个return语句时,这个方法将被结束。
-
与break和continue不同的是,return直接结束整个方法,不管这个return处于多少层循环之内
至此,我们已经学完了流程控制,写完了项目一的实战!再接再厉
==衡量一个功能代码的优劣标准:==
1.正确性
2.可读性
3.健壮性
4.高效率与低存储(时间复杂度、空间复杂度)
eclipse的调配:
1.调整透视图为javaee
2.从搜索框搜索:package exploer、navigator、console加入并放入合适的位置
3.在windows-general-workspace调整字符集为utf-8;在windows-general-appearance-colors and fonts调整字体大小
4.在windows-perspective-customize perspective-menu visibility-file-new调整要加入new菜单的文件
5.可以在 windows-perspective-save perspective as 调节为javaee,这样以后如果透视图乱了,直接windows-perspective-reset perspective即可
eclipse的使用:
1.new一个java文件,new一个包,new一个class
2.从电脑中导入工程:file->import浏览即可,注意运行的各工程不能重名(包括已存在的和将要导入的),重名会导不进来。导入时可以选择copy projects into workspace备份。
3.如何导入源文件:直接复制该源文件,在eclipse某个包里粘贴就行
4..如果不小心开始时勾选了那个以后不再询问,可以在windows->preferences->general->startup and shutdown->workspace处修改
5.工程中的代码有乱码怎么办:出现乱码的代码所使用的字符编码集与工程设置使用的字符编码集不一致,建议修改乱码文件的字符编码集即可
6.注意在每个包下的类的代码要在开头声明你是哪个包的
7.查看源码,鼠标+ctrl
8.如何在编写代码时显示程序员相关信息
第三章 数组
一、数组的概述
1.数组(Array),是多个==相同类型数据==按==一定顺序排列==的集合,并使用==一个名字命名==,通过==编号==的方式对这些数据进行统一管理。
2.数组的常见概念:
-
数组名、下标(索引)、元素、数组的长度
3.数组的特点:
-
数组是有序排列的
-
数组属于引用数据类型的变量,数组的元素既可以是基本数据类型,也可以是引用数据类型
-
创建数组对象会在内存中开辟一整块连续的空间(而链表是典型的非连续空间),而数组名引用的是这块连续空间的首地址。
-
数组的长度,一旦确定就不可修改
-
可以直接通过索引调用指定位置的元素
4.数组的分类
-
按维数分:一维数组、二维数组。。。。
-
按数组元素的类型分:基本数据类型元素的数组、引用数据类型元素的数组
二、一维数组的使用
1.一维数组的声明和初始化
int[] ids; //声明数组名为ids的数组,里面的每个元素都是int型 ids = new int[]{1001,1002,1003,1004};//静态初始化 String[] names; names = new String[5];//动态初始化
-
静态初始化:数组的初始化和数组元素的赋值操作同时进行
-
动态初始化:数组的初始化和数组元素的赋值操作分开进行
错误的写法:
int[] array1 = new int[];//没有初始化 int[5] array2 = new int[5];//定义时不要在[]里写东西 int[] array3 = new int[3]{1,2,3};//不能既静态又动态初始化
-
总结:数组一旦初始化完成,其长度就确定了。
-
没有初始化的数组是不能调用的;
2.如何调用数组的指定位置的元素
-
通过索引(角标、下标)的方式调用
-
数组的索引从0开始,到数组的长度-1结束
-
-
格式:names[0]
names[0] = " "//调用元素来给之赋值
如果索引越界,编译时不会报错,只有在解释运行时分配内存才会报错
3.如何获取数组的长度
-
数组有个属性:length
-
获取长度的方法:数组名.length 返回结果是int型
4.如何遍历数组
for (i = 0 ; i< names.length ; i++){ System.out.println(names[i]); }
5.数组元素的默认初始化值
-
数组元素是整型:默认值是0(包含byte、short、int、long)
-
数组元素是浮点型:默认值0.0(float和double)
-
数组元素是char型,默认值是0(ascii码) 或者''\u0000',不是 ’0‘
-
数组元素是boolean型,默认值是false
-
数组元素是引用数据类型,默认值是null,注意不是”null“,null不是字符串,而是一个关键字
6.数组的内存解析
-
栈:线性结构
-
数组名变量首先在栈中出现,然后堆里出现长度为3的一个数组结构,每个int元素都占用4个字节,栈中的数组名变量指向堆里的数组的首地址,进而构成对应关系
-
输出一个数组,得到的结果是这个数组指向的内存地址
-
地址常用16进制表示;
-
这里的 [ 表示一维,I 表示int,@后面的才是地址
-
当arr1指向别的数组后,原数组会被回收
-
在arr1和arr2所在方法执行完后,arr1和arr2会失去定义,从而被垃圾回收。进而堆中内存也被回收。
三、多维数组的使用
以二维为主
-
二维数组底层的逻辑
理解:
对于二维数组的理解,我们可以看成是一维数组array1又作为另一个一维数组array2的元素而存在。其实,从数组底层的运行机制来看,其实没有多维数组。
==本质上二维数组就是每个元素都是数组的一维数组==
1.二维数组的声明和初始化
int[][] arr1 = new int[][] = {{1,2,3},{4,5},{6,7,8}} //静态初始化 String[][] arr2 = new String[3][2]; //动态初始化 String[][] arr3 = new String[3][]; //动态初始化也可以不写列数,代表已知外层大数组的元素个数,每个元素是一个小数组,但是小数组还没定义; //以下初始化方法是错误的 String[][] arr4 = new String[][2]; String[][] arr4 = new String[][]; String[3][2] arr4 = new String[][]; int[][] arr4 = new int[2][3] = {{1,2,3},{4,5},{6,7,8}}; //以下是一些特殊的正确的写法 int arr5[] = new int[5]; int arr5[][] = new int[3][2]; int []arr5[] = new int[][]{{1,2},{1,2,3},{5,6,7}}; int []arr5 = {1,2,3,4,5};//称为类型推断
二维数组定义时前面的变量类型实际指的是内层元素的类型,外层元素类型是数组
2.如何调用二维数组的指定位置的元素
arr3[3][2];
3.如何获取二维数组的长度
int[][] arr4 = new int [][]={{1,2},{2,3,4}}; arr4.length;//得到arr4的长度是2 arr4[1].length;//得到arr4[1]的长度是3
4.如何遍历二维数组
for (int i = 0;i< arr4.length ; i++){ for ( int j = 0; j< arr4[i].length; j++){ System.out.print(arr4[i][j]+" "); } System.out.println(); }
5.二维数组元素的默认初始化值
事先规定:二维数组分为外层数组的元素和内层数组的元素
int [][] arr = new int [2][3]; 外层元素:arr[0],arr[1]; 内层元素:arr[0][1],arr[1][1]等
输出一个数组名,得到的结果是这个数组名指向的内存地址==(实际上就是把数组名所指向的数组的地址赋值给了数组名)==
因此外层元素的值是地址值(已经确定了,不是默认值),内层元素默认值是变量类型的0
从内存底层看,arr3有4个外层元素,但是每个外层元素没有指向一个数组,且数组是引用数据类型,返回null;由于外层元素没有指向的地方,内层元素更找不到,会报错(空指针异常)
==数组没有初始化就没有分配地址,因此初始化double[4] [ ]没有给 外层元素分配指向的地址,默认值就是null,而内层元素还不存在,空指针异常==
针对于初始化方式1:int[] []arr = new int[4] [3];
-
外层元素的初始化值:地址值
-
内层元素的初始化值:与一维数组初始化情况相同
针对于初始化方式二:int [ ] [ ] arr = new int[4] [ ];
-
外层元素的初始化值:null
-
内层元素的初始化值:不能调用,否则报错
6.二维数组的内存解析
易错点
int[] x,y[];
放在x,y前面的[ ] 是对后面都有作用的;放在y后面的[ ]是只对y有作用的
所以相当于:
int [] x; //x是一维数组 int[] y[]; //y是二维数组
学习宋红康的编程格式,先搭出框架,再去添加功能
无论是遍历一维数组还是遍历二维数组,都要牢牢记住循环条件都是跟数组的长度相关联的。
四、数组中涉及到的常见算法
1.数组元素的赋值(回型数、杨辉三角等)
回型数:
package com.cg.java; import java.util.Scanner; public class RectangleTest{ public static void main(String[]args) { //n用来存放1~n*n的数值,用一个k来代表往那边走 int i = 0, j = 0 , k =1; System.out.println("qingshuru"); Scanner scan = new Scanner(System.in); int n = scan.nextInt();//n*n方阵 int[][] arr = new int[n][n]; for (int m =1 ; m <= n*n ; m++) { if(k == 1) { if (j< arr.length && arr[i][j]== 0) { arr[i][j++] = m; }else { k = 2; j--; m--; i++; } }else if (k == 2) { if (i < n && arr[i][j] == 0) { arr[i++][j] = m; } else { k = 3; i--; j--; m--; } }else if (k == 3) { if (j >= 0 && arr[i][j] == 0) { arr[i][j--] = m; } else { k=4; i--; j++; m--; } }else if (k == 4) { if (i >= 0 && arr[i][j] == 0) { arr[i--][j] = m; } else { k = 1; i++; j++; m--; } } } for (int a = 0 ; a<arr.length; a++) { for (int b = 0; b<arr[a].length; b++) { System.out.print(arr[a][b]+"\t"); } System.out.println(); } } }
杨辉三角:
package com.cg.java; public class yangHui { public static void main(String[]args) { int[][] arr = new int [10][]; for (int i = 0 ; i<arr.length ; i++) { arr[i] = new int [i + 1]; arr[i][i] = arr[i][0] = 1; for (int j = 1 ; j< arr[i].length -1 ; j++) { arr[i][j] = arr[i-1][j-1] + arr[i-1][j]; } } for (int i = 0 ;i< arr.length ; i++) { for (int j = 0; j< arr[i].length; j++) { System.out.print(arr[i][j]+" "); } System.out.println(); } } }
2.求数值型数组中元素的最大值、最小值、平均数、总和等。
package com.cg.java; import java.util.Scanner; public class ArrayTest1 { public static void main(String[]args) { int[] arr = new int [10]; for (int i = 0; i< arr.length ;i ++ ) { arr[i]=(int)(Math.random()*90)+10; System.out.println(arr[i]); } int max = arr[0]; int min =arr[0]; int arg = arr[0]; int sum = 0; for (int i = 0;i< arr.length;i++) { if(arr[i]> max) max = arr[i]; if(arr[i]< min) min = arr[i]; sum += arr[i]; } System.out.println("最大值为:"+max); System.out.println("最小值为:"+min); System.out.println("总和为:" + sum); } }
3.数组的复制、反转、查找(线性查找、二分法查找)
(1)复制:
int array1,array2; array2 = new int[]{2,3,5,7,11,13,17,19}; array2 = array1; for (int i = 0 ; i< array2.length;i++){ if(i % 2 = 0 ){ array2[i] = i; } }
当令array2 = array1时,并没有创建一个新的数组,而是把array1 指向的地址值给了array2,两个指向的是同一个数组
因此这不是真正意义上的复制数组,只是复制了个数组名
思考:array1和array2的关系:array1和array2地址值相同,都指向了堆空间的唯一的一个数组实体,相当于是添加了文件的一个快捷方式
拓展:如何真正的复制一个数组呢?
int []array2 = new int[array1.length]; for(i= 0 ; i< array1.length; i ++){ array2[i] = array1[i]; }
小技巧:new了几个就是有几个数组
这样array1和array2指向的是不同的数组,改变其一不能影响另一个
(2)反转:
package com.cg.java; public class ArrayTest5 { public static void main(String[]args){ int[] arr = new int[] {1,2,3,4,5}; //反转(逆序) for ( int i = 0; i < arr.length/2 ; i++) { int temp = arr[i]; arr[i] = arr[arr.length - 1 - i ]; arr[arr.length - 1 - i ] = temp; } for (int i = 0; i < arr.length;i++) { System.out.print(arr[i]+ "\t"); } } }
(3) 查找(线性查找、二分法查找)[搜索]
-
线性查找比较简单,就是遍历一个一个找
package com.cg.java; public class SearchTest { public static void main(String[]args) { String[] arr = new String[] {"校长","秘书","教师","学生"}; //想查找教师在什么位置,用线性查找 boolean a = false; for (int i = 0; i< arr.length;i++) { if (arr[i].equals("黑丝")) { System.out.println("找到啦,在索引为"+i+ "的位置上"); a = true; } } if(a == false) { System.out.println("很遗憾,没有找到哦!"); } } }
-
二分法查找(比较快)
==前提:所要查找的数组必须有序。==
import java.util.Scanner; public class HalfTest { public static void main(String[]args) { int[] arr= new int[] {-34,-21,-3,5,8,98,546,8667,8674,8796}; int head = 0;//首索引 int end = arr.length - 1;//末索引 boolean isFlag = true; Scanner scan = new Scanner (System.in); System.out.println("请输入你要查找的数"); int index = scan.nextInt(); while (head <= end) { int middle = (head + end) /2; if(arr[middle] == index ) { System.out.println("找到拉,在索引为" + middle +"的位置上"); isFlag = false; break; }else if(arr[middle] < index) { head = middle + 1; }else {//arr[middle] > index end = middle - 1; } } if(isFlag) { System.out.println("很遗憾,没有找到"); } } }
注意,二分查找,每分开一段,首索引要赋值为middle+1或末索引赋值为middle - 1;
4.数组元素的排序算法
何为稳定性?
例如数组{1,7,5,9,3,6,3' } 开始时3在3'前面,排序后为{1,3,3',5,6,7,9},3依旧在3'前面
排序算法的分类:
十大排序算法:
红色的要求会手写,黄色的要求记住思想
算法的·5个特征
冒泡排序:(简单)
public class BubbleSortTest { public static void main(String[]args) { int [] arr = new int[] {1,6,4,12,7456,-2,534,-76,24,15,8,2}; for (int i = 0; i < arr.length - 1; i++) { //i控制需要进行多少轮比较 for ( int j = 0 ; j< arr.length- 1 - i ;j ++) { if(arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1 ] = temp; } } } for (int i = 0 ;i < arr.length ; i++) { System.out.print(arr[i] + "\t"); } } }
快速排序 (快,效率高,但是暂时不好掌握,等系统学算法再说)
==面试常考:排序>赋值>二分查找==
各种方法的比较:
五、Arrays工具类的使用
Arrays工具类中常见的方法
import java.util.Arrays; public class ArraysTest { public static void main(String [] args) { int []arr1 = new int [] {1,2,3,4} ; int [] arr2 = new int [] {1,2,3,4,5}; boolean isEquals = Arrays.equals(arr1 , arr2); System.out.println(isEquals); } } //输出结果为boolean
int []arr1 = new int [] {1,2,3,4}; String arrValue = Arrays.toString(arr1); System.out.println(arrValue); //输出结果为[1,2,3,4]
int []arr1 = new int [] {1,2,3,4}; Arrays.fill(arr1, 10); System.out.println(Arrays.toString(arr1)); //输出结果为[10,10,10,10]
int []arr1 = new int [] {4,5,3,8}; Arrays.sort(arr1); System.out.println(Arrays.toString(arr1)); //输出结果为[3,4,5,8]
int []arr1 = new int [] {4,5,3,8}; int a = Arrays.binarySearch(arr1,5); System.out.println(a); //输出结果为1
六、数组使用中的常见异常
1.数组角标越界的异常:ArrayIndexOutOfBoundsExcetion
左边越界和右边越界都算越界
2.空指针异常: NullPointerException
第四章、面向对象编程(上)
一、面向对象概述
学习面向对象内容的三条主线:
-
Java类及类的成员:属性、方法、构造器;代码块、内部类
-
面向对象的三大特征:封装、继承、多态 、(抽象性)
-
其他关键字:this 、super、static、final、abstract、interface、package、import 等
面向过程和面向对象的区别:
1.面向过程(POP):强调的是功能行为,以函数为最小单位,考虑怎么做
2.面向对象(OOP):强调具备了功能的对象,以类/对象为最小单位,考虑谁来做
二、java语言中的基本元素:类和对象
1.设计类,其实就是设计类的成员
属性 = 成员变量 = field = 域、字段
方法 = 成员方法 = 函数 = method
生活中描述事物无非就是描述事物的属性和行为。如人有身高、体重等属性, 有说话,打球等行为
创建类的对象 = 类的实例化 = 实例化类
2.类和对象的使用:(面向对象思想落地的实现)
(1)创建类,设计类的成员(属性、方法)
(2)创建类的对象
(3)通过“对象.属性”或“对象.方法”调用对象的结构。
public class PersonTest { public static void main(String [] args) { //创建对象(类的实例化) Person p1 = new Person(); //调用对象的结构:属性、方法 //(1)调用属性:对象名.属性 p1.isMale = true; p1.name = "马云"; p1.age = 19; System.out.println(p1.name +"nianlingshi"+p1.age); Person p2 = new Person(); p2.name = "王健林"; p2.age = 199; p2.isMale = false; System.out.println(p2.name +"nianlingshi"+ p2.age); //(2)调用方法:变量名.方法 p1.eat(); p2.eat(); p1.speak("English"); p2.speak("Chinese"); } } //创建一个类及他的成员(属性、方法) class Person{ int age ; boolean isMale ; String name ; public void eat() { System.out.println("人可以吃饭"); } public void sleep() { System.out.println("人可以睡觉"); } public void speak(String language) { System.out.println("人可以用" + language + "说话"); } }
3. 如果创建了一个类的多个对象,则每个对象都独立的拥有一套类的属性。(非static的)
-
意味着,如果我们修改一个对象的属性a,则不影响另外一个对象属性a的值。
4.对象的内存解析
对象名存储的内容是该对象指向堆空间的地址。
==当一个方法执行结束时,定义在其中的局部变量会出栈、被回收(典型的就是main方法)。==
==main方法结束后,其中创建的对象会被回收,先是栈空间的对象名被回收,堆空间存在的属性没有指针指向了,也就被回收==
5.类中属性的使用
属性(成员变量) vs 局部变量
(1)不同点:
-
在类中声明的位置不同:
-
属性:直接定义在类的一对{ }中
-
局部变量:声明在方法内、方法形参、代码块内、构造器形参、构造器内部的变量。
-
class User{ //属性(成员变量) public String name ; int age ; boolean isMale ; //方法 public void talk(String language){//形参,是局部变量 System.out.println("我们使用"+ language ); } public void eat(){ String food = "烙饼"; //方法内的,是局部变量 System.out.println("我们喜欢吃"+ food ); } }
-
关于权限修饰符的不同:
-
属性:可以在声明属性时,指明其权限,使用权限修饰符
-
常用的权限修饰符:private 、public 、缺省 、 protected ,到封装性时再细说,目前声明属性使用缺省即可。
-
-
局部变量:不可以使用权限修饰符
-
-
默认初始化值的情况:
-
属性:类的属性,根据其类型,都有默认初始化值
-
整型(byte 、short 、int 、long ),0
-
浮点型(float 、double ),0.0
-
字符型(char),0(或\u0000)
-
布尔型(boolean),false
-
引用数据类型(类、接口、数组):null
-
-
局部变量:没有默认初始化值
-
意味着,在调用局部变量之前,一定要显式赋值
-
特别地:形参在调用时,我们赋值即可
-
-
-
在内存中加载的位置
-
属性:加载到堆空间(非static)
-
局部变量:加载到栈空间
-
(2)相同点
-
定义变量的格式: 数据类型 变量名 = 变量值
-
先声明,再使用
-
变量都有其对应的作用域
6.类中方法的声明和使用
方法:描述类应该具有的功能。
比如:Math类:sqrt() / random() / ...
Scanner类:next() / nextInt() / nextXxx() / ...
Arrays类:sort() / binarySearch() / toString() / equals() / fill()...
(1)方法的分类:
(2)方法的声明:
权限修饰符 返回值类型 方法名 (形参列表){
方法体
}
public static void main (String[]args){ System.out.println("woshizhu1"); }
常见关键字: static 、final 、 abstract 来修饰的方法 这些后面再说
(3)说明
-
关于权限修饰符:
-
Java规定的4种权限修饰符:private 、public 、缺省 、 protected
-
权限修饰符到封装性时再细说,目前声明方法均使用public
-
-
返回值类型:有返回值 vs 无返回值
-
-
如果方法有返回值,则必须在方法声明时,指定返回值的类型。同时,方法中需要使用return关键字来返回指定类型的变量或常量。
-
如果方法没有返回值,则方法声明时,使用void来表示。通常没有返回值的方法中不需要使用return关键字,但是,如果使用的话,只能使用 return; 来表示此方法结束。
-
return后不能有表达式,否则会报错,跟break和continue类似
-
-
如何判断定义方法该不该有返回值?
-
题目要求
-
依靠经验
-
-
-
方法名:属于标识符,遵循标识符的规则、规范、见名知意
-
形参列表:方法可以声明0、1、或多个形参。
-
格式: 数据类型1 形参1 ,数据类型2 形参2 , 。。。
-
如何判断定义方法该不该有形参?
-
题目要求
-
依靠经验
-
-
-
方法体:方法功能的体现。
(4)return关键字的使用:
-
使用范围:使用在方法体中
-
作用:(1)结束方法
(2)针对于有返回值的方法,使用“return 数据”方式返回数据
-
return后不能有表达式,否则会报错,跟break和continue类似
(5)方法的使用:
-
==可以调用当前类的属性和方法==:static的只能调用static的
-
一般的方法都是调用了别的方法,特殊的,方法A中又调用了方法A:递归方法
-
方法中,不可以定义方法
如果在一个类中,如果方法需要用到属性的值,那么方法不建议写带参数的,直接用属性的值就可
class Square { //半径是圆的一个属性 double radius; public double square() { double s = radius*radius*Math.PI; return s; } }
每个类都是一个类型
比如class People,
==对象数组==
数组的元素可以是任何类型,当然也可以是对象
//比如已经有了一个class People People[] arr = new People [20]; //此时生成了一个20个元素的数组,每个元素都是People类型的,但是没有new对象,元素的默认初始值为null。 for (int i = 0 ; i <arr.length ; i++){ arr[i] = new People ();//给每个元素new一个对象,相当于给数组赋值。 }
==对象数组第一代:==
public class Test4 { public static void main(String[]args) { Students [] arr = new Students[20]; for(int i = 0; i < arr.length ; i++) { arr[i] = new Students(); arr[i].score = (int)(Math.random()*101); // int (Math.random()*(b - a + 1) + a) arr[i].state = (int)(Math.random()*6+1); System.out.println(arr[i].state+ " "+ arr[i].score); } System.out.println("年级为3的人有以下的几个"); for(int i = 0; i < arr.length ; i++) { if (arr[i].state == 3) { System.out.println("学号为"+ (i+1) + "成绩为"+ arr[i].score); } } //冒泡排序 for (int i = 0 ; i< arr.length -1 ; i++) { for (int j = 0 ; j < arr.length - 1 - i ; j++) { if( arr[j].score > arr[j+1].score) { //定义一个同类型的元素,才能赋值 Students temp = arr[j]; arr[j] = arr[j+1]; arr[j+ 1] = temp; } } } for(int i = 0; i < arr.length ; i++) { System.out.println("学号为\t"+ (i+1) + "年级为\t"+ arr[i].state+"成绩为\t"+ arr[i].score); } } } class Students { int number; int state; int score; }
==对象数组升级版==
==把主方法里的功能都封装起来,然后在主函数中调用==
public class Test5 { public static void main(String[]args) { Students1 [] arr = new Students1[20]; Test5 test = new Test5(); test.give(arr); test.print(arr); test.bubbleSort(arr); test.result(arr); } //功能1:给对象的属性赋值 public void give (Students1 arr[]) { for(int i = 0; i < arr.length ; i++) { arr[i] = new Students1(); arr[i].number = i + 1; arr[i].score = (int)(Math.random()*101); arr[i].state = (int)(Math.random()*6+1); System.out.println(arr[i].state+ " "+ arr[i].score); } } //功能2:输出学号为3的学生的信息 public void print (Students1 arr[]) { System.out.println("年级为3的人有以下的几个"); for(int i = 0; i < arr.length ; i++) { if (arr[i].state == 3) { System.out.println("学号为"+ (i+1) + "成绩为"+ arr[i].score); } } } //功能三:用成绩给学生排序 public void bubbleSort(Students1 arr[]) { //冒泡排序 for (int i = 0 ; i< arr.length -1 ; i++) { for (int j = 0 ; j < arr.length - 1 - i ; j++) { if( arr[j].score >arr[j+1].score) { //定义一个同类型的元素,才能赋值 Students1 temp = arr[j]; arr[j] = arr[j+1]; arr[j+ 1] = temp; } } } } //功能4:遍历 public void result (Students1 arr[]) { for(int i = 0; i < arr.length ; i++) { System.out.println("学号为\t"+ arr[i].number + "年级为\t"+ arr[i].state+"成绩为\t"+ arr[i].score); } } } class Students1 { int number; int state; int score; }
==补充知识==
分配内存空间是在运行的时候做的,编译只是生成了一个class文件。
编译完源程序之后,生成一个或多个字节码文件。我们使用jvm中的类的加载器和解释器对生成的字节码文件进行解释运行。意味着,需要将字节码文件对应的类加载到内存中,涉及到内存解析。
JVM内存结构:
虚拟机栈:即平时提到的栈结构,局部变量都存储在栈结构中、
堆:将new出来的结构(数组、对象)加载在堆空间中。对象的属性(非static的)记载在堆空间中。
方法区:类的加载信息、常量池、静态域。
回顾变量的分类:
(1).按数据类型来分:基本数据类型:byte、short、int、long、double、float、boolean、char
引用数据类型:类、接口、数组
(2).按照在类中声明的位置:属性 vs 局部变量
-
成员变量:
-
实例变量:不以static修饰
-
类变量:以static修饰
-
-
局部变量:
-
形参(方法、构造器中定义的变量)
-
方法局部变量(在方法内定义)
-
代码块局部变量(在代码块内定义)
-
理解“万事万物皆对象”:
-
在java语言中,我们都将功能、结构等封装到类中,通过类的实例化,来调用具体的功能结构
-
Scanner、String等常用类
-
涉及到文件操作,也是把文件作为类File
-
网络资源:URL
-
-
涉及到Java语言与前端(HTML)、后端的数据库交互时,前后端的结构都体现为类、对象。
内存解析的说明
(1)==引用数据类型只可能存储两类值:null和地址值(地址值包含了该数据的类型)==
匿名对象
-
理解:我们创建的对象,没有显式的赋给一个变量名,即为匿名对象。
-
特征:匿名对象只能调用一次。
-
使用:
//没有起名操作直接new 对象,而new对象的目的就是调用方法 new People().sendEmail; new People().playGame; //注:上述两个对象不是同一个,因为new出来的是新对象
如上的用法是匿名对象的主要用处,这里的new Phone()赋给了形参phone,所以sendEmail和playGame是同一个对象的属性
自定义数组的工具类:
-
之前做过数组排序、查找、逆序、复制等操作。现在可以把这些作为方法封装到Array类中
-
为什么要封装起来呢?一方面为了后续检查、修改代码;另一方面提高代码的复用性,比如另一个程序也需要用到同样的方法,就可以直接拿去用了。
7.再谈方法
(1)方法的重载(Overload)
-
定义:在同一个类中,允许存在一个以上的同名方法,只要他们的参数个数或参数类型不同即可。
-
举例:api文档中有很多重载的方法
-
作用:重载是为了满足对不同类型的参数进行相同操作
-
两同 一不同:
-
同一个类、相同方法名
-
参数列表不同:参数个数不同、参数类型不同、参数顺序不同
-
-
判断是否是重载:
-
跟方法的权限修饰符、返回值类型、形参变量名、方法体都没有关系!根据定义:只有定义在同一个类中的方法名相同、参数列表不同的方法才是重载的方法。
-
重载中可能出现自动类型提升:(传入的参数如果没有类型正好对应的方法,可以去找类型能匹配(自动提升)的方法)
-
在通过对象调用方法时,如何确定某一个指定的方法?
-
先找方法名,再找类型匹配的
-
某网红题:
为啥会这样勒?
因为这是println的重载方法,两个调用的根本不是一个方法,当调用第一个println时,作用是输出内容(地址值),而第二个方法是遍历输出数组(实际上是改写了toString方法)
(2)可变个数的形参(Varargs)(Variable number of arguments)
jdk5.0新增的内容
-
具体使用:
-
可变个数形参的格式:数据类型 ... 变量名
-
当调用可变个数形参的方法时,传入的参数个数可以是:0个、1个、或多个
-
可变个数形参的方法与本类中方法名相同,形参不同的方法之间构成重载。
-
可变个数形参的方法与本类中方法名相同,形参类型也相同的数组不能共存,
-
实际上可变个数形参可以看作是数组参数的简化,因为可变个数形参在调用时不用再new一个数组。
-
这个形参可以用作数组,访问索引,具有length属性(说白了就是数组,只是简化了一点)
-
可变个数形参在方法的形参中,必须声明在末尾,由此有推论:可变个数形参在方法的形参中,最多只能声明一个可变形参
-
(3)==方法参数的值传递机制==
-
关于变量的==赋值==:如果变量是基本数据类型,此时赋值的是变量所保存的数据值 ; 如果变量是引用数据类型,此时赋值的是变量所保存的数据的地址值。
-
方法的形参的传递机制:值传递
-
形参:方法定义时,声明的小括号内的参数。
实参:方法调用时,实际传递给形参的值。
-
值传递机制:
-
如果参数是基本数据类型,此时实参赋给形参的是实参真实存储的值
-
如果参数是引用数据类型,此时实参赋给形参的是实参存储数据的地址值(包含变量的数据类型)(真实的对象实体(数组实体)在堆空间中的首地址值)
-
-
-
==调用函数进行实参传递给形参时,在内存中的体现是在栈空间中新开辟了一块区域给形参,然后将实参的值(数据、地址)赋值给形参==
(4)递归(recursion)方法
-
递归方法:一个方法体内调用它自身
-
方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制
-
递归一定要向已知方向递归,否则这种递归就变成了无穷递归,类似死循环
-
递归的典型应用:斐波那契数列(上台阶问题)、汉诺塔问题、快排
三、面向对象的特征之一:封装和隐藏
-
我们程序设计追求”高内聚、低耦合“。
-
隐藏对象内部的复杂性,只对外公布简单的接口。把该隐藏的隐藏起来,该暴露的暴露出来,就是封装性的设计思想
1.问题的引入:当我们创建一个类的对象后,我们可以通过”对象.属性“的方式对对象进行赋值,这里,赋值操作要受到属性的数据类型和存储范围的制约。但是没有其他制约条件,但是在实际问题中,我们往往需要给属性赋值加入额外的限制条件。这个条件就不能在属性声明时体现,我们只能通过方法进行限制条件的添加。同时,我们==需要避免用户再使用”对象.属性“的方式进行赋值==,则需要将属性声明为私有的(private),此时,针对于属性就体现了封装性。
2.封装性的体现(不等同与封装性,只是一个体现而已):
我们将类的属性私有化(private),同时,提供公共的(public)方法来获取(getXxx)和设置(setXxx)此属性的值。
拓展:封装性的其他体现:
不对外暴露的私有的方法、单例模式(将构造器初始化,不能创建新对象了,只有一个实例)、如果不想在别的包下调用某个类,可以把他设置为缺省的。
3.封装性的体现需要权限修饰符来配合。
(1)java规定的4种权限(从小到大排列):private、缺省、protected、public
(2)具体适用范围:
(3)4种权限可以用来修饰类及类的内部结构:属性、方法、构造器、内部类(不含代码块哦)
(4)具体的
-
修饰类的话,只能用public和缺省
-
修饰类的内部结构的话(除了代码块之外的4种),4种修饰符都可以。
一个知识:在同一个包下不能定义同名的类,不同的包内可以定义同名的类
4.总结封装性:Java提供了4种权限修饰符来修饰类及类的内部结构在被调用时的可见性的大小。
四、类的成员第三:构造器(构造方法)(constructor)
1.构造器的作用:
-
创建对象:Person p = new Person( );
-
初始化对象的属性:Person p = new Person("Tom");
实际上是new + 构造器
2.说明:
-
如果没有显式的定义类的构造器的话,则系统默认提供一个空参的构造器,==该构造器的权限修饰符与类的权限修饰符相同==
-
定义构造器的结构:权限修饰符 类名(形参列表){ }
-
一个类中定义的多个构造器是彼此重载的。
-
一旦我们显式的定义了类的构造器之后,系统就不再提供默认的空参构造器。
-
==一个类中,至少会有一个构造器==
五、总结:属性的赋值过程
属性的可能赋值方法:
①:默认初始化
②:显式初始化
③:构造器中初始化
④:通过“对象.方法”(setName ( ) )或“对象.属性”的方式赋值
以上操作的先后顺序:①—>②—>③—>④,即后面的会覆盖前面的
六、JavaBean的使用
JavaBean是一种Java语言写成的可重用组件。
所谓JavaBean,是指符合如下标准的Java类:
-
类是==公共==的
-
有一个无参的==公共==的构造器
-
有属性,且有对应的get、set方法
七、UML类图
八、this关键字的使用
1.this可以用来修饰、调用:属性、方法、构造器
2.this修饰属性和方法:this理解为当前对象 或 当前正在创建的对象
2.1 在类的方法中,我们可以使用“this.属性”或“this.方法”的方式,调用当前对象属性或方法,但是,通常情况下,我们都选择省略“this.”,特殊情况下,如果方法的形参和类的属性同名时,我们必须显式的使用“this.变量”的方式,表明此变量是属性,而非形参。
2.2在类的构造器中,我们可以使用“this.属性”或“this.方法”的方式,调用当前正在创建的对象属性或方法,但是,通常情况下,我们都选择省略“this.”,特殊情况下,如果构造器的形参和类的属性同名时,我们必须显式的使用“this.变量”的方式,表明此变量是属性,而非形参。
3.this修饰、调用构造器:
-
我们在类的构造器中,可以显式的使用“this(形参列表)”方式,调用本类中指定的其他构造器。
-
构造器中不能通过“this(形参列表)”方式调用自己。
-
不管用this调用了几个其他的构造器,一次使用new只构造了一个对象
-
如果一个类中有n个构造器,则最多有n-1个构造器中使用了“this(形参列表)”
-
规定:“this(形参列表)”必须声明在当前构造器的首行
-
构造器内部、最多只能声明一个“this(形参列表)”,用来调用其他的构造器
-
this跳到其他构造器中,减少代码的冗余
九、关键字:package、import的使用
1.package关键字的使用
-
为了更好的实现项目中类的管理,提供包的概念
-
使用package声明类或接口所属的包,声明在源文件的首行(首行是指第一行,前面可以有空行)
-
包,属于标识符,符合标识符的命名规则规范(全小写)、见名知意
-
每“.”一次,就代表一层文件目录
-
补充:同一个包下,不能命名同名的接口、类,不同的包下可以命名同名的接口、类。
MVC设计模式
十、import关键字的使用
import : 导入
1.在源文件中显式的使用import结构导入指定包下的类、接口。
2.声明在包的声明和类的声明之间;
3.如果需要导入多个结构,则并列写出即可。
4.可以使用 xxx.* 的方式表示可以导入xxx包下的所有结构。
5.如果使用的类或接口是java.lang包下定义的,则可以省略import结构
6.如果使用的类或接口是本包下定义的,则可以省略 结构
7.如果在源文件中,使用了不同包下的同名的类,则必须至少有一个类需要以全类名的方式显示。例如“java.util.Xxx”;
8.使用“xxx.*”方式表明可以调用xxx包下的所有结构,但是如果使用的是xxx子包下的结构,则仍需要显式导入
9.import static :导入指定类或接口中的静态结构:属性或方法。
==在同一个类中可以不创建对象直接使用方法,但是在主函数或者其他类中需要创建对象==
第五章、面向对象编程(中)
一、面向对象的特征之二:继承性
1.继承性的好处:
-
减少代码冗余,提高代码复用性
-
便于功能的拓展
-
为之后多态性的使用,提供了前提
2.继承性的格式:class A extends B{
}
-
A称为:子类、派生类、subclass
-
B称为:父类、超类、基类、superclass
-
体现:一旦子类A继承父类B以后,子类A中就获取了父类B中声明的结构:属性、方法
-
特别的,==父类中声明为private的属性或方法,子类继承父类以后,仍然认为获取了父类中私有的结构,只是因为封装性的影响,使得子类不能直接调用父类的结构而已==。
-
-
子类继承父类以后,还能声明自己特有的属性或方法:实现功能的拓展。因此子类的功能更丰富
-
extends的意思:拓展
3.Java中关于继承性的规定:
-
一个类可以被多个类继承。
-
Java中==类的单继承性==:一个类只能有一个父类
-
子父类是相对的概念。
-
子类直接继承的父类称为直接父类,间接继承的父类称为间接父类,这样多层次的继承被称为多层继承
-
子类继承父类以后,就获取了直接父类以及所有间接父类中声明的属性和方法。
4.
-
如果我们没有显式的声明一个类的父类的话,则此类继承于java.lang.Object类
-
所有的java类(除java.lang.Object类)都直接或间接的继承于java.lang.Object类。
-
意味着,所有的java类具有java.lang.Object类声明的功能
关于注释:
-
在类前、方法前、属性前:文档注释
-
逻辑步骤前:单行、多行注释
如何调试程序
1.System.out.println();时不时加入输出语句判断何处出现错误
2.Eclipse 的debug调试功能 (debug as java application)
3.常用操作
二、方法的重写(override/overwrite)
1.定义:在子类中可以根据需要对从父类中==继承==来的方法进行改造,声明一个与父类==同名同参数==的方法,也称为方法的重置、覆盖。在程序执行时,子类的方法将覆盖父类的方法。
2.应用:重写以后,当创建子类对象以后,通过子类对象调用子父类中的同名同参数的方法时,实际执行的是子类重写父类的方法
3.重写的规定:
-
方法的声明:权限修饰符 返回值类型 方法名(形参列表){
方法体
}
-
约定俗成:子类中的叫重写的方法,父类中的叫被重写的方法
-
注:
-
子类重写的方法的方法名和形参列表与父类被重写的方法的方法名和形参列表相同
-
子类重写的方法的==权限修饰符不小于==父类中被重写的方法的权限修饰符
-
特殊的:==子类不能重写父类中权限修饰符为private的方法==(只是创建了一个新的方法,并未覆盖掉父类中的方法)
-
-
返回值类型:
-
父类被重写的方法的返回值类型是void,则子类重写的方法的返回值类型只能是==void==
-
父类被重写的方法的返回值类型是A类型,则子类重写的方法的返回值类型可以是==A类或A类的子类==
-
父类被重写的方法的返回值类型是基本数据类型,则子类重写的方法的返回值类型必须是==相同==的基本数据类型。
-
-
子类重写的方法抛出的==异常类型不大于==父类被重写的方法抛出的异常类型(具体放到异常处理的时候讲)
-
平时开发重写方法时,直接复制粘贴准没错。
-
==子类和父类中的同名同参数的方法要么都声明非static的(考虑重写),要么都声明为static的(不是重写),如果一个是static,一个是非static的,不能重写==
==经典面试题==:
区分方法的重载和重写:(就是说明重载和重写到底是什么,有哪些细节说清楚即可)
三、四种访问权限修饰符
protected指的是父类的方法可以在与父类所在包的不同包下的子类中使用,在普通类(非子类)中不能使用
四、关键字:super
==首先,需要知道的是如果子父类中有同名同类型属性,不会像方法一样覆盖父类中的属性==
super关键字的使用:
1.super理解为:父类的
2.super可以用来调用:属性、方法、构造器
3.super的使用(调用属性和方法)
-
我们可以在子类的方法或构造器中。通过使用“super.属性”或"super.方法"的方式,显式的调用父类中声明的属性或方法,但是,通常情况下,我们习惯省略“super.”
-
特殊的,当子类和父类中定义了同名的属性时,我们要想在子类中调用父类中声明的属性,则必须显式使用"super.属性"的方式,表明调用的是父类中声明的属性。
-
特殊的,当子类重写了父类中的方法以后,我们想在子类的方法中调用父类中被重写的方法时,则必须显式的使用“super.方法”的方式,表明调用的是父类中被重写的方法。
4.super调用构造器
-
我们可以在子类的构造器中显式的使用"super(形参列表)"的方式,调用父类中声明的指定的构造器
-
=="super(形参列表)"的使用,必须声明在子类构造器的首行!==
-
我们在类的构造器中,针对于"this(形参列表)"或"super(形参列表)"只能二选一,不能同时出现。因为只有一个首行,而他们都需要放在首行,就矛盾了,而且这两个是必然出现一个的。
-
在构造器的首行,没有显式的声明"this(形参列表)"或"super(形参列表)",则==默认调用的是父类中空参的构造器“super()”.==如果父类只显式的定义了非空参数列表的构造器,那么在使用子类时就会报错,因为子类构造器中默认调用"super()",但是在父类中找不到空参构造器
-
在类的多个构造器中,至少有一个构造器中使用了"super(形参列表)"调用父类中的构造器,因为n个构造器最多只能有n-1个this(),因为再多就会构成环,类似死循环,所以至少有一个是super(形参列表).
五、子类实例化对象全过程
1.从结果上来看:
-
子类继承父类以后,就获取了父类中声明的属性和方法。
-
创建子类的对象,在堆空间中,就会加载所有父类中声明的属性。
2.从过程上来看:
-
当我们通过子类的构造器创建子类对象时,我们一定会直接或间接的调用其父类的构造器,进而调用父类的父类的构造器,直到调用了java.lang.Object类中空参的构造器为止。正因为加载过所有父类的结构,所以才可以看到内存中有父类的结构,子类对象才可以考虑调用。
-
明确一点,虽然创建子类对象时,调用了父类的构造器,但是没有new,自始至终就创建过一个对象,即为new的子类对象
3.思考为什么super和this调用语句只能作为构造器中的第一句出现?
无论通过哪个构造器创建子类对象,需要先保证初始化父类
目的:当子类继承父类后,继承父类中所有的属性和方法,因此子类有必要知道父类如何为对象进行初始化。
六、面向对象特征之三:多态性
1.理解多态性:可以理解为一个事物的多种形态
(实现了代码的通用性)
2.何为多态性:
对象的多态性:父类的引用指向子类的对象(或子类的对象赋给父类的引用)
Person p2 = new Man();//其中Man是Person的一个子类
p2:父类的引用 new Man() :子类的对象
3.多态的使用:当调用==子父类同名参数的方法==时,实际执行的是子类重写父类的方法
p2.eat(); p2.walk();//实际上调用的是子类重写的方法
有了对象的多态性以后,我们在编译期,只能调用父类中声明的方法,但在运行期,我们实际执行的是子类重写父类的方法。———虚拟方法调用
总结:==编译,看左边;运行,看右边。==
4.多态性的使用前提:
-
要有类的继承关系
-
要有方法的重写(否则没必要new子类对象了,直接new父类的得了)
5.对象的多态性只适用于方法,不适用于属性(编译和运行都看左边)
6.多态是运行时行为,不是编译时行为
7.小结:经典面试题
==方法的重载和重写==
==多态是编译时行为还是运行时行为?==
答:运行时行为
如何证明?
写出父类以及一系列改写了父类中同一个方法的子类,在测试类中编写参数为父类类型的方法,然后在main方法中调用,利用随机数的产生来传入不同子类的参数,实际调用的方法在编译时并不能确定,只有在运行时才知道调用的到底是哪个方法。
==final、finally、finalize的区别?==
====和equals的区别==
==三元运算符和if语句==
==关于Integer源码==
==谈谈你对多态性的理解?==
不好解释,就举抽象类、接口、数据库这些例子说明,如果没有多态性,只能用父类对象作为参数,有了多态性,可以传入子类对象的参数,进而调用方法时,虽然编译时方法体中的方法仍是以父类为参数,但是运行时就是子类改写后的方法。
8.向下转型
Person p2 = new Man();//其中Man是Person的一个子类 //假设Man类中有work方法而Person类中没有,那我们不能使用p2.work()来使用work方法
有了对象的多态性以后,内存中实际上是加载了子类特有的属性和方法的,但是由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性和方法,子类特有的属性和方法不能调用。
那么,如何才能调用子类特有的属性和方法?
向下转型:使用强制类型转换符()
Man m1 = (Man)p2;//其中p2是Person类型
为什么能把p2转成Man类型呢?因为p2在内存中实际存在的对象就是Man类型的,如果要把p2转换成Woman运行时会出现异常。
个人关于向下转型的理解:假设这样的继承关系Object ->Person -> Student
执行Object p = new Student(); 创建了Object类的变量,但实际上指向Student类型的对象,如果执行Person per = (Person) p;程序会正常运行,为什么?因为这里的p实际上还是Student类型的对象,将Student类型对象转成Person类型不正符合吗。
使用强转时,可能出现ClassCastException的异常,为了避免出现这样的问题,引入关键字instanceof。
9.instanceof关键字的使用:
-
a instanceof A:判断对象a是否是类A的实例。如果是,返回true,如果不算,返回false。
-
使用情境:为了避免在向下转型时出现ClassCastException异常,我们在向下转型之前,先进行instanceof判断,一旦返回true,就进行向下转型,如果返回false,不进行向下转型
-
如果a instanceof A返回true,则a instanceof B 也返回true,其中B是A的父类,即a instanceof A为true当且仅当A为a所在类或a所在类的父类,当且仅当a是A类或A的子类
在进行强制转换时几种常见现象解析:
问题一:编译时通过,运行时不通过:
Person p3 = new Woman(); Man m3 = (Man)p3;//编译时把p3当作Person类,向下转型为Man类 //但是运行时p3实际上还是Woman类的实例对象,Woman不能转换称Man。因此会报错
问题二:编译不通过
如果将两个没有子父类关系的变量强转,编译就会出现问题
Woman m1 = new Woman(12); Man m = (Man)m1;
编译和运行都能过:
Object obj = new Woman(); Person p = (Person)obj;
七、Object类的使用
1.Object类是所有Java类的根父类,属于java.lang包下
2.如果在类的声明中未使用extends关键字指明其父类,则默认父类为java.lang.Object类
3.Object类中的功能(属性、方法)就具有通用性
4.Object类没有属性,只声明了一个空参的构造器
5.方法:equals()、tostring()、getclass()、hashCode()、clone()、finalize()、wait()、notify()、notifyAll();
-
实际上数组也可以融入类这个大家庭中,数组也是继承于Object类的,具有Object的方法。而接口则是独立于类存在的。
6.面试题:==和equals()区别
(1)回顾==的使用:
-
可以使用在基本数据类型变量和引用数据类型变量中
-
如果比较的是基本数据类型变量,比较的是两个变量保存的数据是否相等。(不一定类型要相同)
-
如果比较的是引用数据类型变量,比较的是两个变量保存的地址值是否相同,即两个引用是否指向同一个对象实体。
-
注:==不同类型变量(除自动类型提升)之间不能 == ,编译会报错==,必须保证符合左右两边变量类型一致
==注意:对于两个字符串比较是否相等,是比较特殊的,如果用equals()方法,判断的是两个字符串内容是否相等; 如果用 == ,判断的是地址是否相等。但是,关键是如果两个字符串是String a = “a”,String b = “a”这样赋值的, 他们指向常量池中的地址应该是一样的,所以此时返回true,而如果他们是new String(“a”)赋值的,他们的地址就不一样,返回false。==
(2)equals()方法的使用:
-
是一个方法,而不是一个运算符
-
只能适用于引用数据类型
-
Object类中equals()的定义:
-
public boolean equals(Object obj) { return (this == obj); }
-
说明:==Object类中定义的equals()和 == 的作用是相同的,即比较两个对象的地址值是否相同,即两个引用是否指向同一个对象实体==
-
-
像String、Date、File、包装类等都重写了Object类中的equals()方法。重写之后,比较的不是两个引用的地址是否相同,而是比较两个对象的 ”实体内容“ 是否相同。
-
通常情况下,我们自定义的类如果适用equals()的话,也通常是比较两个对象的“实体内容”是否相同。那么,我们就需要对Object类中的equals()方法重写。
-
重写的原则:比较两个对象的实体内容是否相同(一般是属性是否相同)
-
还可以用eclipse自动生成的equals(),
-
-
注:==如果某个类的属性是自定义类,要对该类进行改写equals(),注意在判断自定义类的属性是否相等时,也需要改写自定义类中的equals()方法==
7.toString()方法
(1)当我们输出一个对象的引用时,实际上就是调用当前对象的toString().
实际上看源码我们能知道println()方法中就调用了toString()方法,所以才有上面的结论。(前提:调用toString()方法的对象不是null,如果是null的话,直接输出得到null,调用toString()方法得到空指针异常)
(2)Object类中的toString()方法的定义。
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
(3)像String、Date、File、包装类等都重写了Object类中的toString()方法,使得在调用对象的toString()时,返回"实体内容信息"。例如String类型的对象调用toString()方法时,就回返回字符串的具体值
(4)自定义类也可以重写toString()方法,当调用此方法时,返回对象的“实体内容”。
八、包装类(Wrapper)的使用
1.Java中的 JUnit 单元测试的使用:
步骤:
-
选中当前工程 -> 右键选择:build path ->add libraries- >JUnit4->下一步
-
创建一个java类进行单元测试
-
此时的java类要求:
-
此类是公共的
-
这个类要提供一个公共的无参的构造器(可以用默认的)
-
-
-
此类中声明单元测试方法:
-
此时的单元测试方法:方法的权限是public,没有返回值,没有形参
-
-
此单元测试方法上需要声明注解:@Test,并在单元测试类中导入:import org.junit.Test;
-
声明好单元测试方法以后,就可以在方法体内测试相关的代码。
-
写完代码以后,左键双击单元测试方法名,右键run as-> Junit Test
说明:(1)如果执行结果没有任何异常:绿条
(2)如果执行结果出现异常:红条
-
单元测试方法可以直接输出执行结果(类似main()),不同的是单元测试方法可以直接调用本类中的属性和方法而不需要造对象。//原因是main函数是static的,不能调用非静态的方法
2.包装类(Wrapper)的使用
包装类的含义:把基本数据类型作为某个类的属性来出现,进而实现将基本数据类型包装在类中,就可以实现真正的面向对象
主要研究基本数据类型、包装类、String类之间的转换
(1)基本数据类型-->包装类
将基本数据类型作为类的属性出现。输出时默认调用包装类中改写后的toString方法,输出相应的值。
Integer i1 = new Integer(1); Integer i2 = new Integer("1"); Float f1 = new Float(12.3f); Boolean b1 = new Boolean(false); Boolean b2 = new Boolean ("TRUE");//属性值为TRUE Boolean b3 = new Boolean ("true123")//属性值为false,详情见看源码
(2)包装类--->基本数据类型
Integer i1 = new Integer(1); int int1 = i1.intValue(); System.out.println(int1 + 2);//输出3
==自动装箱和自动拆箱==(JDK5.0新特性)
自动装箱:基本数据类型-->包装类
int num2 = 10; Integer i1 = num2;//自动装箱 boolean b1 = false; Boolean bool = b1;//自动装箱
自动拆箱:包装类-->基本数据类型
Integer i1 = num2; int int1 = i1;
自动装箱或拆箱之后,int型和Integer型可以直接替代对方作用,以下为例:
Integer i1 = 3; syso(i1+1);//Integer型可以自动拆箱然后与基本类型进行算术运算
//定义一个方法,形参为Object型 public void test(Object obj){ } //在main方法中调用该方法时 main(){ //假设已创建对象te //这时我们想将3这个数字作为test方法的参数输入,发现可以直接输入,这就是自动装箱 te.test(3); }
(3)基本数据类型、包装类-----> String类型(因为有自动装(拆)箱,所以把基本类型和包装类看成一个整体了)
//方式一:连接运算 String str1 = num1 + "";
//方式二:调用String的valueOf(Xxx xxx)方法 int i1 = 12; syso(Stirng.valueOf(i1));//参数是基本类型 Integer int1 = i1; syso(String.valueOf(int1));//参数是包装类的对象
(4)String类型---->基本数据类型、包装类。
==错误的情况:==
String str1 = new String("123"); int num1 = (int)str1; Integer n1 = (Integer)str1;//强转不能用在没有子父类关系之间转换
==正确的方法==:调用包装类的parseXxx()方法
String str1 = new String("123"); int num1 = Integer.parseInt(str);
可能会报NumberFormatException的异常。
关于多态性、包装类的一个综合题,其中用了集合里的Vector类
package com.cg.java2; import java.util.Scanner; import java.util.Vector; public class ScoreTest { public static void main(String[] args) { Vector v = new Vector(); System.out.println("请输入学生们的成绩"); Scanner scan = new Scanner(System.in); int max = 0; while(true) { int score = scan.nextInt(); if (score == -1) { break; } //老方法 /*Integer intScore = score; v.addElement(intScore);//多态*/ //现方法 v.addElement(score);//自动装箱 if(max < score) { max = score; } } int newScore = 0; char level; for (int i = 0 ; i < v.size() ; i++) { if (v.elementAt(i) instanceof Integer) { newScore = (Integer) v.elementAt(i); if (max - newScore < 10) { level = 'A'; System.out.println("第"+(i+1)+"个学生分数为" + newScore + "\t等级为"+ level); }else if (max - newScore < 20) { level = 'B'; System.out.println("第"+(i+1)+"个学生分数为" + newScore + "\t等级为"+ level); }else if (max - newScore < 30) { level = 'C'; System.out.println("第"+(i+1)+"个学生分数为" + newScore + "\t等级为"+ level); }else{ level = 'D'; System.out.println("第"+(i+1)+"个学生分数为" + newScore + "\t等级为"+ level); } } } } }
第六章、面向对象(下)
一、static关键字的使用
1.static: 静态的
2.static可以用来修饰:属性、方法、代码块、内部类
3.使用static修饰属性:静态变量(或类变量)
-
属性:按是否使用static修饰,又分为 静态属性和非静态属性(实例变量)
-
(非静态属性)实例变量:我们创建了类的多个对象,每个对象都独立的拥有一套类中的非静态属性,当修改其中一个对象的非静态属性时,不会导致其他对象中同样的属性值的修改。
-
(静态属性)静态变量(类变量):我们创建了类的多个对象,多个对象共享同一个静态变量,当通过某一个对象修改静态变量时,会导致其他对象调用此静态变量时,是修改过了的。
-
-
static修饰属性的其他说明
-
静态变量随着类的加载而加载,可以通过"类.静态变量"的方式进行调用。
-
静态变量的加载要早于对象的创建。
-
由于类只会加载一次,则静态变量的内存也只会存在一份,存在方法区的静态域中。
-
-
静态属性举例:System.out ; Math.PI
-
类变量 VS 实例变量 的内存解析
4.static修饰方法:静态方法
-
静态方法随着类的加载而加载,可以通过"类.静态方法"的方式调用
-
静态方法中只能调用静态的方法或属性,因为他们的生命周期是一样的;非静态方法中,既可以调用非静态的方法或属性,也可以调用静态的方法或属性。
5.static注意点:
-
在静态的方法内,不能使用this关键字、super关键字.//因为this表示的是当前对象,而静态变量创建时并没有初始化对象
-
关于静态属性和静态方法的使用,都从生命周期的角度去理解
6.在开发中,如何确定一个属性是否要声明为static的?
-
属性是可以被多个对象所共享的,不会随着对象的不同而不同的,适合加static
-
类中的常量也常常声明为static的。
7.在开发中,如何确定一个方法是否要声明为static的?
-
操作静态属性的方法通常设置为static的
-
工具类中的方法,习惯上声明为static的。比如Math、Arrays、Collections
8.==单例设计模式==
-
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例
(1)饿汉式:
-
私有化类的构造器
-
类的内部创建该类的对象
-
提供公共的方法,返回类的对象
-
要求此对象也必须声明为静态的
(2)懒汉式:
-
私有化类的构造器
-
类的内部声明该类的对象,但不初始化
-
提供公共的方法,返回类的对象
-
要求此对象也必须声明为静态的
(3)区分饿汉式和懒汉式
饿汉式:坏处:对象加载时间过长。
好处:饿汉式是线程安全的。
懒汉式:好处:延迟对象的创建。
目前写法的坏处:线程不安全(到多线程内容时,再修改)
二、理解main方法的语法
main()方法的使用说明
1.main()方法作为程序的入口
2.mian()方法也是一个普通的静态方法
3.为啥main()方法要是public的?
-
因为jvm运行时早跳出了这个文件,所以需要main方法权限大一些
4.为啥main()方法要是static的?
-
因为jvm就可以直接拿类来实现main方法,而不用创建对象
5.main()方法也可以作为我们与控制台交互的方式。(之前我们使用的都是Scanner类)
三、类的成员第四:代码块(Block)
代码块(或初始化块)
1.代码块的作用:用来初始化类、对象
2.代码块如果有修饰的话,只能使用static
3.分类:静态代码块 VS 非静态代码块
4.静态代码块
-
内部可以有输出语句
-
随着类的加载而执行,而且只执行一次。
-
初始化类的信息(静态属性)
-
如果一个类中定义了多个静态代码块,则按照声明的先后顺序执行
-
静态代码块的执行要优先于非静态代码块的执行
-
静态代码块内只能调用静态的属性、方法。不能调用非静态的结构
5.非静态代码块
-
内部可以有输出语句
-
随着对象的创建而执行,而且每创建一个对象就执行一次非静态代码块
-
作用:可以在创建对象时,对对象的属性等进行初始化
-
如果一个类中定义了多个非静态代码块,则按照声明的先后顺序执行
-
非静态代码块内可以调用静态的属性、方法,或非静态的属性、方法
如果有几个类之间有继承关系,每个类又都有相应的静态和非静态的代码块,那么在实例化子类对象时,这些代码块的执行顺序是怎样的呢?
-
==由父及子,静态先行==
-
按照main函数里的顺序,若main函数中有先执行的输出语句,那就先执行main的,如果在创建子类对象之后才有输出语句,那就先输出子类信息,再执行输出语句。
-
当创建该子类对象时,先加载父类的静态代码块->子类的静态代码块->父类的非静态代码块->父类的构造器->子类的非静态代码块->子类的构造器(要注意构造器和非静态代码块的先后顺序并不影响他们的执行顺序,都是非静态代码块优先于构造器的)
6.属性的赋值先后顺序:
默认赋值-->显式赋值、代码块赋值(谁先赋值取决于二者的位置,上面的先执行)-->构造器赋值-->对象.属性或对象.方法赋值 (代码块是优先于构造器执行的,这个和上面的输出执行语句也是一致的)
其中显式赋值和代码块赋值谁后出现就是后赋值
四、final关键字
final : 最终的
1.final可以用来修饰的结构:类、方法、变量
2.final用来修饰一个类:表明此类不能被其他类所继承。比如:String类、System类、StringBuffer类。
3.final用来修饰一个方法:表明此方法不能被子类重写。比如Object类中getClass();
4.final 用来修饰变量:此时的“变量”就称为是一个常量
-
final修饰属性:可以考虑赋值的位置有:显式初始化、代码块中初始化、构造器初始化
-
final修饰局部变量:
-
方法内:表明该常量不可修改
-
方法形参:表明该形参是一个常量。当我们调用此方法时,给常量形参赋一个实参,一旦赋值以后,就只能在方法体内使用此形参,但不能进行重新赋值,(此形参可以是任意类型的,如果是自定义类,那么不能在方法内部重新new对象赋给他)。
-
static final : ( 1 )修饰属性:全局常量(static修饰的属性称为全局变量,static final 修饰的属性称为全局常量)
(2)修饰方法:不能被重写的静态方法。
权限修饰符能修饰的:类 、 属性、方法、构造器、内部类
static能修饰的:属性、方法、代码块、内部类
final能修饰的:类、方法、变量(属性、局部变量)
小结:
五、抽象类和抽象方法
abstract关键字的使用
1.abstract: 抽象的
2.abstract可以用来修饰的结构:类、方法
3.abstract修饰类:抽象类。
-
此类不能实例化
-
抽象类中一定有构造器,便于子类实例化时调用(涉及:子类对象实例化的全过程)
-
开发中,都会提供抽象类的子类,让子类对象实例化,完成相关的操作
4.abstract修饰方法:抽象方法
-
抽象方法只有方法的声明,没有方法体
-
==包含抽象方法的类一定是抽象类==,反之,抽象类中可以没有抽象方法。
-
若子类重写了父类中的所有抽象方法后,此子类方可实例化
-
若子类没有重写子类中的所有抽象方法,则此子类也是一个抽象类,需要用abstract修饰
5.实例,在求几何图形面积的例子中,由于不知道几何图形的形状,不能求面积,所以要将求面积的方法设为抽象的。即,父类中不能明确表达出方法体的,建议写成抽象方法。
6.abstract使用上的注意点:
(1)abstract不能用来修饰:属性、构造器等结构。
(2)abstract不能用来修饰私有方法,静态方法,final的方法、final的类。
-
因为abstract修饰方法之后,是希望他的子类进行重写该方法的,但是私有方法不能重写,静态方法不会被覆盖,final不能重写;final的类无法继承,那我又不能造对象,又不能继承,要我何用!
7.抽象类的匿名子类:
//创建一个抽象类,里面含抽象方法 abstract class Person{ public Person() { } public abstract void eat(); public abstract void sleep(); } //前面学过匿名对象,即直接new ,而不赋给变量 //抽象类还可以匿名子类,格式如下 class Test1{ public static void main(String []args){ Person p = new Person(){ public void eat(){ System.out.println("好好的吃饭"); } public void sleep(){ System.out.println("好好的睡觉"); } }; method(p); } public static void method(Person p){ p.eat(); p.sleep(); } }
8.抽象有什么用处呢?
-
将父类中不确定的部分抽象化,在子类中进行改写。
9.模板方法的设计模式
10.官方语言中覆盖父类中非抽象的方法叫做重写(override),而覆盖父类中的抽象方法叫做实现(implement),接口中的方法也类似称为实现
六、接口(Interface)
1.接口使用interface来定义
2.Java中,接口和类是并列的两个结构
3.如何定义接口:定义接口的成员
-
JDK7及以前:接口中只能定义全局常量和抽象方法
-
全局常量:public static final的,但是书写时可以省略,默认了。
-
抽象方法:public abstract的,但是书写时可以省略,默认了。
-
-
JDK8:除了定义全局常量和抽象方法之外,还可以定义静态方法、默认方法。
4.接口中是不能定义构造器的,意味着接口是不能实例化的。
5.Java开发中,接口通过让类去实现(implements)的方式来使用,如果实现类覆盖了接口中的所有抽象方法,则此实现类就可以实例化,如果实现类没有覆盖接口中所有的抽象方法,则此实现类仍为一个抽象类。
6.Java类可以实现多个接口,------>弥补了Java单继承性的局限性
接口也可以继承其他接口
继承和接口的格式:class AA extends BB implements CC,DD,EE{ }
7.接口与接口之间可以继承,而且可以多继承。
类与类之间只能单继承 类可以实现多个接口 接口可以继承多个接口
8.接口的具体使用,体现多态性
9.接口实际上可以看作是一种规范
10.开发中,体会面向接口编程
11.接口的应用:代理模式、工厂模式
12.接口中的属性,方法都是public 的,就算省略也是public 而不是缺省
13.JDK8以后的接口:除了定义全局常量和抽象方法之外,还可以定义静态方法、默认方法。
格式:
public interface Com{ public static void method1(){//静态方法 sout("666"); } public default void method2(){//默认方法 sout("777"); } }
-
==接口中定义的静态方法,只能通过接口来调用,不能通过实现类的对象调用,这与父类中的静态方法可以给子类对象调用不一样哦。==
-
通过实现类的对象,可以调用接口中的默认方法。当然实现类也能重写接口中的默认方法,调用时调用的是重写后的方法
-
如果子类(实现类)继承的父类和实现的接口中声明了同名同参数的默认方法,那么子类在没有重写此方法的情况下,默认调用的是父类中的同名同参数的方法。------>称为类优先原则 (只针对接口和父类中的同名同参数作用,对接口和接口之间的类,接口和类之间的属性均不适)
-
如果子类(实现类)继承的父类和实现的接口中声明了同名的属性,子类直接调用属性时会报错,因为没法判断究竟是哪个属性
-
如果实现类实现了多个接口,而多个接口中定义了同名同参数的==默认方法==,那么在实现类没有重写此方法的情况下,报错----->接口冲突。这就需要我们必须在实现类中重写该方法。
-
如何在子类(实现类)的方法中调用父类、接口中被重写的方法?
-
显然父类中的可以用"super.方法"调用
-
而接口中的,用"接口名.super.方法"调用(规定)
-
14.抽象类、接口等结构中含有抽象方法,这样的方法在被子类覆盖时称为实现,有方法体的方法被子类覆盖时叫重写
小笔试题:
(1)如何调用父类和接口中的同名属性
(2)接口中的属性在实现类中不能修改。
==面试题:抽象类与接口与哪些异同==
相同点:
-
都不能实例化对象
-
都可以定义抽象方法
不同点:
-
把抽象类和接口(java7、java8、java9)的定义、内部结构解释说明
-
抽象类有构造器,接口没有
-
类是单继承性、接口可以多继承;一个类可以实现多个接口
七、类的成员之五:内部类
1.Java中允许将一个类A声明在另一个类B中,则类A就是内部类,类B称为外部类。
2.内部类的分类:成员内部类(静态、非静态) VS 局部内部类(方法体内、构造器内、代码块内)
3.成员内部类:
-
一方面,作为外部类的成员:
-
可以调用外部类的结构
-
class Person1 { public void eat(){ System.out.println("人能吃饭"); } //非静态成员内部类 class Hand { public void sing(){ System.out.println("我能唱歌"); eat();//调用外部类的结构 Person1.this.eat();//是eat()的全貌 } }
-
可以用static修饰 (外部类是不能用static修饰的)
-
可以用4种权限修饰符修饰 (外部类只能被2种权限修饰符修饰)
-
-
另一方面,作为一个类:
-
类内可以定义属性、方法、构造器等。
-
可以被final修饰,表示此类不能被继承,言外之意,不用final,就可以被继承
-
可以被abstract修饰,表示此类不能实例化
-
4.关注以下3个问题
(1) 如何实例化成员内部类的对象
//在类Person中定义了Dog和Bird两个内部类,其中前者是静态的,后者是非静态的 main(S){ //创建Dog实例,(静态的成员内部类) Person.Dog dog = new Person.Dog(); dog.show(); //创建Bird实例(非静态的成员内部类) Person person = new Person(); Person.Bird bird = person.new Bird(); bird.sing(); }
(2)如何在成员内部类中区分调用外部类的结构
在成员内部类的方法中,如何分别调用方法的参数、内部类的属性、外部类的属性呢?
System.out.println(age);//形参 System.out.println(this.age);//内部类的属性 System.out.println(Person.this.age);//外部类的属性
(3)开发中局部内部类的使用
成员内部类和局部内部类,在编译以后,都会生成字节码文件
格式:成员内部类:外部类名$内部类名.class
局部内部类:外部类名$数字 内部类名.class
注意点:在局部内部类的方法中如果调用内部类所声明的方法中的局部变量,要求此局部变量声明为final的。
JDK 7及以前要求此局部变量显式声明为final的,
JDK 8及以后的版本,可以省略final的声明。
第七章、异常处理
一、异常概述
1.异常:在Java语言中,将程序执行中发生的不正常情况称为异常,(语法错误和逻辑错误不是异常)
2.异常事件可分为两类:
-
Error : Java虚拟机无法解决的严重问题,如:JVM系统内部错误、资源耗尽等严重情况。如StackOverflowError(栈溢出)和OOM(OutOfMemoryError)(堆溢出),一般不编写针对性的代码进行处理。
-
Exception(意外)(狭义上的异常):其他因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。如
-
空指针访问
-
试图读取不存在的文件
-
网络连接中断
-
数组角标越界
-
二、异常体系结构和常见异常:
==面试题:常见的异常有哪些?举例说明==
-
java.lang.Throwable
-
java.lang.Error:一般不编写针对性的代码进行处理
-
java.lang.Exception:可以进行异常的处理
-
编译时异常(checked):
-
IOException
-
FileNotFoundException
-
-
ClassNotFoundException
-
-
运行时异常(unchecked,RuntimeException)
-
NullPointerException
-
IndexOutOfBoundsException
-
ArrayIndexOutOfBoundsException
-
StringIndexOutOfBoundsException
-
-
ClassCastException
-
NumberFormatException
-
InputMismatchException
-
ArithmeticException
-
-
-
三、异常处理机制一:try-catch-finally
1.异常的处理:抓抛模型
过程一:抛:程序在正常执行的过程中,一旦出现异常,就会在异常代码处生成一个对应异常类的对象,并将此对象抛出。
一旦抛出对象以后,其后的代码就不再执行。
过程二:抓:可以理解为异常的处理方式:try-catch-finally和throws
2.try-catch-finally的使用
格式:
try{ //可能出现异常的代码 }catch(异常类型1 变量名1){ //处理异常的方式1 }catch(异常类型2 变量名2){ //处理异常的方式2 }catch(异常类型3 变量名3){ //处理异常的方式3 } ....... finally{ //一定会执行的代码 }
说明:
(1)finally是可选的
(2)使用try将可能出现异常的代码包装起来,在执行过程中,一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,去catch中进行匹配
(3)一旦try中的异常对象匹配到某一个catch时,就进入catch中进行异常的处理,一旦处理完成,就跳出当前的try-catch结构(在没有写finally的情况下),==继续执行其后的代码==
(4)catch中的异常类型如果没有子父类关系,则谁声明在上,谁声明在下无所谓
catch中的异常类型如果满足子父类关系,则要求子类一定声明在父类的上面,否则,报错。
(5)常用的异常对象处理的方式:
-
String getMessage();
catch(Exception e){ sout(e.getMessage()); }
-
printStackTrace()
catch(Exception e){ e.printStackTrace(); }
(6)在try结构中声明的对象,在出了try结构以后,就不能再调用。(如果想在try结构和外部都能用同一个变量,那就在try之前声明该变量并赋初始值)。
(7)try-catch-finally结构是可以嵌套的。
3.try-catch-finally中finally的使用
(1)finally是可选的
(2)finally中声明的是一定会被执行的代码,即使catch中又出现异常了,try中有return语句,catch中有return语句,try中异常没有被catch抓住,等情况
(3)什么时候用finally呢?
-
像数据连接,输入输出流,网络编程中的socket等资源,JVM是不能自动回收的,我们需要自己手动的进行资源的释放,此时的资源释放,就需要声明在finally中。
体会1:使用try-catch-finally处理编译时异常,使得程序在编译时就不再报错,但是运行时仍可能报错,相当于我们使用try-catch-finally将一个编译时可能出现的异常,延迟到运行时出现。
体会2:开发中,由于运行时异常比较常见,所以我们通常就不针对运行时异常编写try-catch-finally了,针对于编译时异常,我们一定要考虑异常的处理。
四、异常处理机制二:throws + 异常类型
1.“throws + 异常类型”写在==方法的声明==处,指明此方法执行时,可能会抛出的异常类型。一旦当方法体执行时,出现异常,仍会在异常代码处生成一个异常类的对象,此对象满足throws后异常类型时,就会被抛出。==异常代码后续的代码就不再执行!==
2.体会:try-catch-finally:真正的将异常给处理掉了。
throws的方式只是将异常抛给了==方法的调用者==,并没有真正将异常处理掉。
3.子类重写父类的方法时,抛出的异常要不大于父类抛出的异常
4.开发中如何选择try-catch-finally 还是使用throws?
-
如果父类中被重写的方法没有throws方式处理异常,则子类重写的方法也不能使用throws,意味着如果子类重写的方法中有异常,必须使用try-catch-finally方式处理。
-
执行的方法中,先后又调用了另外的几个方法,这几个方法又是递进关系执行的(前一个方法被后一个方法调用),我们建议这几个方法使用throws的方式进行处理,而执行的最终方法a可以考虑使用try-catch-finally方式进行处理。
5.try-catch-finally 和 throws 只能选择一个使用。
五、手动抛出异常
关于异常对象的产生:
-
系统自动生成的异常对象
-
手动的生成一个异常对象,并抛出(throw)
格式:
throw new RuntimeException("出现异常"); throw new Exception("出现异常+1");
六、用户自定义异常类
如何自定义异常类?
(1)继承于现有的异常结构:RuntimeException或Exception(继承谁就代表有谁的功能)
(2)提供全局常量serialVersionUID,表示序列号
(3)提供重载的构造器
几个易错点:
-
编译时异常是除了RuntimeException之外的Exception类及其子类的对象
-
编译时异常必须被捕获处理,不然无法通过编译。
-
运行时异常可以不被处理,程序应该从逻辑角度尽可能避免这类异常的发生。
-
自定义异常如果抛出的是Exception异常,则必须要捕获
面试题:
final、finally、finalize三者的区别
throw 和 throws的区别
-
throw:生成一个异常对象,并抛出。使用在方法内部
-
throws:是处理异常的一种方式。使用在方法声明处的末尾。
-
上游排污,下游治污。一个是生成异常的,一个是处理异常的。
Collection 和 Collections的区别
String 、StringBuffer 、 StringBuilder的区别
ArrayList、LinkedList的区别
HashMap、LinkedHansMap的区别
重写、重载的区别
抽象类和接口的异同
==和equals()的区别
sleep()和wait()的区别
第八章、多线程和idea的使用
idea的基本使用
配置略,康师傅有详细教程
-
eclipse在不同的工作区(workspace)中的配置(metadata)不同,需要重新配置,而idea不同project的配置文件是共用的,不需要重新配置
-
idea中一个窗口只能打开一个project,而一个project里面可以创建不同的模块,负责不同的功能,例如京东商城为一个project,里面的秒杀,抢购就可能分别由两个模块实现。
-
可以自己在idea中创建很多模板,有关于idea不会用的可以再看尚硅谷idea的基本配置教程
一、基本概念:程序、进程、线程
-
程序是为完成特定任务、用某种语言编写的一组指令的集合。即指==一段静态的代码==,静态对象。
-
进程是程序的一次执行过程,或是==正在运行的一个程序==。是一个动态的过程:有他自身的产生、存在和消亡的过程-----生命周期
-
如:运行中的qq,运行中的MP3播放器
-
程序是静态的,进程是动态的
-
进程作为==资源分配的单位==,系统在运行时会为每个进程分配不同的内存区域
-
-
线程,进程可进一步细化为线程,是一个程序内部的一条执行路径。
-
若一个进程同一时间并行执行多个线程,就是支持多线程的。
-
线程作为调度和执行的单位,==每个线程拥有独立的运行栈和程序计数器(pc),==线程切换的开销小
-
==一个进程中的多个线程共享相同的内存单元/内存地址空间==-->它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效 (何为通信:通信就是两个线程都能操作同一空间的东西) 。但每个线程操作共享的系统资源可能就会带来安全隐患。
-
-
单核CPU和多核CPU的理解
-
单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。
-
如果是多核的话,才能更好的发挥多线程的效率(现在的服务器都是多核的)
-
一个Java程序java.exe,其实至少有三个线程 : main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
-
-
并行与并发:
-
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事
-
并发:一个CPU(采用时间片)同时执行多个任务,比如:秒杀、多个人做同一件事
-
-
使用多线程的优点:
-
以单核cpu为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?
-
多线程程序的优点:
-
1.提高应用程序的响应。对图形化界面更有意义,可增强用户体验
-
提高计算机系统CPU的利用率。
-
改善程序结构,将既长又复杂的进程分成多个线程,多个运行,利于理解和修改
-
-
-
何时需要多线程?
-
程序需要同时执行两个或多个任务
-
程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
-
需要一些后台运行的程序时。
-
二、线程的创建和使用
1.线程的创建和启动
-
Java语言的JVM运行程序运行多个线程,它通过java.lang.Thread 类来体现
-
Thread类的特性
-
每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体。
-
通过该Thread对象的start()方法来启动这个线程,而非直接调用run().
-
2.创建多线程
(1)方法一:
多线程的创建,方式一:继承于Thread类 * 1.创建一个继承于Thread类的子类 * 2.重写Thread类的run() ,将此线程要执行的操作声明在run()中 * 3.创建Thread类的子类的对象 * 4.通过此对象调用Start():启动当前线程;调用当前线程的run()。
-
-
问题1:我们不能通过直接调用run()的方式启动线程,如果是直接调用run(),就没有启动一个新的线程,仍然是处于main线程中
-
问题2:再启动一个线程时,不可以还让已经start()的线程去执行,会报IllgalThreadStateException.。我们需要重新创建一个线程的对象,再去调用start().
-
由于只需要调用一次start()方法,所以用匿名子类的方法比较简洁。
-
public class ThreadTest { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); MyThread myThread1 = new MyThread(); myThread1.start(); //以下操作仍然是在main线程中执行的 for (int i = 0; i < 100; i++) { if (i %2 != 0){ System.out.println(Thread.currentThread().getName() + ":" +i); } } } } class MyThread extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { if (i %2 == 0){ System.out.println(Thread.currentThread().getName() + ":" +i); } } } }
在讲创建多线程的方法二之前,先学习一下Thread类中的一些方法
-
1.start():启动当前线程;调用当前线程的run() * 2.run():通常需要重写Thread类中的此方法,将创建的线程需要执行的操作声明在其中 * 3.currentThread():静态方法,返回执行当前代码的线程 * 4.getName():获取当前线程的名字 //如果是自己创建的线程,直接对象名.getName()或者setName();如果是在main中,可以用Thread.currentThread.getName()或者Thread.currentThread.setName(). * 5.setName():设置当前线程的名字 //给线程命名除了setName()方法外还有新建构造器的方法。
6.yield():释放当前cpu的执行权,但是可能会由回到该线程 * 7.join():在线程a中调用线程b的join()方法,此时线程a就进入阻塞状态,直到线程b完全执行完后, * 线程a才结束阻塞状态。 * 8.stop():已过时。当执行此方法时,强制结束当前线程,不推荐使用 * 9.sleep(long millitime):让当前线程"睡眠"指定的millitime毫秒。 * 在指定的millitime毫秒时间内,当前线程是阻塞状态 * 10.isAlive():判断当前线程是否存活
线程的调度:
-
调度策略
-
线程的优先级:
-
线程的优先等级:
-
MAX_PRIORITY:10
-
MIN_PRIORITY:1
-
NORM_PRIORITY:5
-
-
涉及的方法:
-
getPriority():返回线程优先级
-
setPriority(int newPriority):改变线程的优先级
-
-
-
说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完,低优先级的线程才执行。
(2).方法二:
创建多线程的方式二:实现Runnable接口 * 1.创建一个实现了Runnable接口的类 * 2.实现类去实现Runnbale中的抽象方法:run() * 3.创建实现类的实例对象 * 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 * 5.通过Thread类的对象调用start()。//启动线程;调用当前线程的run()-->调用了Runnable类型的target的run().
//1.创建一个实现了Runnable接口的类 class MThread implements Runnable{ // 2.实现类去实现Runnbale中的抽象方法:run() @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0){ System.out.println(i); } } } } public class ThreadTest1 { public static void main(String[] args) { //3.创建实现类的实例对象 MThread mThread = new MThread(); //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 Thread thread = new Thread(mThread); //5.通过Thread类的对象调用start()。 thread.start(); } }
//run的源码 public void run() { if (target != null) { target.run(); } }
-
再启动一个线程时,我们需要重新创建一个Thread类的对象,而不需再创建Runnable的实现类的对象,再去调用start()即可.
比较创建线程的两种方式:
-
开发中优先选择实现Runnable接口的方式
原因:
-
实现的方式没有类的单继承性的局限
-
实现的方式更适合来处理多个线程有共享数据的情况、
-
-
联系:public class Thread implements Runnable ,即Thread也是实现了Runnable接口的
-
相同点:俩种方式都需要重写run(),将线程要执行的逻辑声明在run()中
三、线程的生命周期
四、线程的同步
* 1.问题:卖票过程中,出现了重票、错票-->出现了线程安全问题 * 2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票 * 3.如何解决:当一个线程a在操作ticket时,其他线程不能参与进来,直到线程a操作完ticket,其他线程才可以操作ticket * 这种情况即使线程a出现了阻塞,也不能被改变 * 4.在java中,我们通过同步机制,来解决线程的安全问题 * 方式一:同步代码块 * synchronized(同步监视器){ * //需要被同步的代码 * } * 说明:1.操作共享数据的代码,即为需要被同步的代码。 ->大括号中不能包含代码多了,也不能包含代码少了 * 2.共享数据:多个线程共同操作的变量。比如本题中的ticket * 3.同步监视器,俗称:锁。任何一个类的对象,都能充当锁 * 要求:多个线程必须要共用同一把锁(必须在内存中是同一个变量,不能是run一次新建个对象) * * 补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器 * 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,可以考虑使用 类名.class 充当同步监视器 * * 方式二:同步方法 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的,再在run()中调用该方法即可 * 1.同步方法仍然涉及到同步监视器,只是不需要我们显式的声明 * 2.非静态的同步方法,同步监视器是:this; * 静态的同步方法,同步监视器是:当前类本身,类名.class * * 5.同步的方式,解决了线程的安全问题-->好处 * 操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低-->局限性
No1.用代码块同步实现接口类的安全
class Window1 implements Runnable{ int ticket = 200; @Override public void run() { while (true){ synchronized (this) { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + ":" + ticket); ticket--; } else { break; } } } } } public class SafeImplementsByBlock { public static void main(String[] args) { Window1 window1 = new Window1(); Thread t1 = new Thread(window1); Thread t2 = new Thread(window1); Thread t3 = new Thread(window1); Thread t4 = new Thread(window1); t1.start(); t2.start(); t3.start(); t4.start(); } }
No2.用方法同步实现接口类的安全
class Window2 implements Runnable{ int ticket = 1000; @Override public void run() { while (true){ show(); if (ticket <= 0){ break; } } } private synchronized void show() { if (ticket > 0){ System.out.println(Thread.currentThread().getName()+":"+ticket); ticket--; } } } public class SafeImplementsByMethod { public static void main(String[] args) { Window2 window2 = new Window2(); Thread t1 = new Thread(window2); Thread t2 = new Thread(window2); Thread t3 = new Thread(window2); Thread t4 = new Thread(window2); t1.start(); t2.start(); t3.start(); t4.start(); } }
No3.用代码块同步实现继承类的安全
class Window3 extends Thread{ private static int ticket = 1000; @Override public void run() { while (true ){ synchronized (Window3.class){ if (ticket > 0){ System.out.println(getName()+":"+ticket); ticket--; }else { break; } } } } } public class SafeExtendsByBlock { public static void main(String[] args) { Window3 w1 = new Window3(); Window3 w2 = new Window3(); Window3 w3 = new Window3(); w1.start(); w2.start(); w3.start(); } }
No4.用方法同步实现继承类的安全
class Window4 extends Thread{ private static int ticket = 1000; @Override public void run() { while (true){ show(); if (ticket <= 0){ break; } } } private static synchronized void show() {//注意这里的static,如果没有static,默认同步监视器是this,这样的话·多个线程就不是同一个锁了 if (ticket > 0){ System.out.println(Thread.currentThread().getName()+":"+ticket); ticket --; } } } public class SafeExtendsByMethod { public static void main(String[] args) { Window4 w1 = new Window4(); Window4 w2 = new Window4(); Window4 w3 = new Window4(); w1.start(); w2.start(); w3.start(); } }
手写线程安全的懒汉式单例模式
//手写一个线程安全的单例模式 //懒汉式: class Single{ private Single(){ } private static Single single; public static Single getSingle(){ //效率较高 if (single == null){ synchronized (Single.class){ if (single == null){ single = new Single(); } } } return single; } }
关于多线程的一个个人理解:
调用的都是不同对象的start()方法,开启了不同的线程,但是这些线程可能都会作用与同一个数据,进而需要同步synchronized,保证多个线程不会同时操作一个数据。
一个synchronized里面只能进入一个线程,其他线程在外面等待。如果这些对象中的run()方法功能一样,也就是说多个人抢着干同一件事;如果这些对象的run()方法功能不一样,就是多个人抢着干不同的事。
-
关于死锁的理解
1.死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁 * 2,说明: * 1)出现死锁后,不会出现异常,不会出现提示,只是所有线程都处于阻塞状态,无法继续 * 2)我们使用同步时,要避免出现死锁
public class ThreadTest { public static void main(String[] args) { StringBuffer s1 = new StringBuffer(); StringBuffer s2 = new StringBuffer(); new Thread(){ @Override public void run() { synchronized (s1){ s1.append("a"); s2.append("1"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s2){ s1.append("b"); s2.append("2"); System.out.println(s1); System.out.println(s2); } } } }.start(); new Thread(new Runnable() { @Override public void run() { synchronized (s2){ s1.append("c"); s2.append("3"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s1){ s1.append("d"); s2.append("4"); System.out.println(s1); System.out.println(s2); } } } }).start(); } }
-
解决死锁的办法
-
专门的算法、原则
-
尽量减少同步资源的定义
-
尽量避免嵌套同步
-
3.Lock(锁)
-
从JDK5.0开始,Java提供了更强大的线程同步机制---通过显式定义同步锁对象来实现同步。同步锁对象使用Lock对象充当
-
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
-
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
使用Lock锁的步骤:
class Window implements Runnable{ private int ticket = 100; //1.实例化ReentrantLock private ReentrantLock lock = new ReentrantLock(true); @Override public void run() { while (true){ try { //2.调用锁定方法lock() lock.lock(); if (ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":"+ticket); ticket--; } }finally { //3.调用解锁方法:unlock() lock.unlock(); } } } } public class LockTesty { public static void main(String[] args) { Window window = new Window(); Thread t1 = new Thread(window); Thread t2 = new Thread(window); Thread t3 = new Thread(window); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
==面试题:synchronized 与 Lock的异同:==
-
相同点:二者都可以解决线程安全问题
-
不同点:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器,Lock需要手动的启动同步( Lock() ),同时结束同步也需要手动的实现( unlock() )。
==面试题:如何解决线程安全问题:==
synchronized同步代码块、synchronized同步方法、Lock锁
是否有多线程? -->有多个线程做同一件事或者多个线程做不同的事都是多线程
是否有线程安全问题?--->多个线程操作共享数据,就会出现线程问题
五、线程的通信
线程通信的例子:使用两个线程打印1-100.线程1,2交替打印 * 涉及到的方法: * wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器 * * notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个 * * notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。 * 说明: * 1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。 * 2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器 * 否则,会出现IllegalMonitorStateExceprio xxxn异常 * 3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中
class Number implements Runnable{ private int number = 1; private Object object = new Object(); @Override public void run() { while (true){ synchronized(object){ object.notify(); if (number < 100){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":" + number); number++; try {//使得调用如下wait()方法的线程进入阻塞状态 object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else { break; } } } } } public class CommunicationTest { public static void main(String[] args) { Number number = new Number(); Thread t1 = new Thread(number); Thread t2 = new Thread(number); t1.setName("线程1"); t2.setName("线程2"); t1.start(); t2.start(); } }
面试题==sleep()和wait()的异同==
-
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态
-
不同点:
-
两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()
-
调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或同步方法中。
-
关于是否释放同步监视器:如果两个方法都使用在同步方法或同步代码块中,sleep()不会释放锁,wait()会释放锁。
-
六、JDK5.0新增线程创建方式
1.新增方式一:实现Callable接口
-
与使用Runnable接口相比,Callable功能更强大些。
-
相比run()方法,可以有返回值
-
方法可以抛出异常
-
支持泛型的返回值
-
需要借助FutureTask类,比如获取返回结果
-
//1.创建一个实现Callable接口的实现类 class NumThread implements Callable { int i = 0; //2.实现call方法,将此线程需要执行的操作声明在call()中 @Override public Object call() throws Exception { int sum = 0 ; while (true) { if (i % 2 == 0) { Thread.sleep(100); System.out.println(Thread.currentThread().getName() + ":" + i); sum += i; } i ++; if (i == 100){ break; } } return sum; } } 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 t1 = new Thread(futureTask); Thread t2 = new Thread(futureTask); t1.setName("线程1"); t2.setName("线程2"); t1.start(); t2.start(); int a = 0; try { //6.获取Callable中的call()的返回值 //7.get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值 a = (int) futureTask.get(); System.out.println(a); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
如何理解实现Callable接口的方式 创建多线程比实现Runnable接口创建多线程方式强大?
-
call()可以有返回值。
-
call()可以抛出异常,被外面的操作捕获,获取异常的信息
-
Callable是支持泛型的
2.新增方式二: 使用线程池
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用
(1)使用线程池的好处:
-
提高响应速度(减少了创建新线程的时间)
-
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
-
便于线程管理
-
corePoolSize:核心池的大小
-
maximumPoolSize:最大线程数
-
keepAliveTime:线程没有任务时最多保持多长时间后会终止
-
(2)线程池相关API:
JDK5.0起提供了线程池API:ExecutorService和Executors
class NumberThread implements Callable { @Override public Object call () throws Exception{ for (int i = 0; i < 100; i++) { if (i % 2 == 0){ System.out.println(Thread.currentThread().getName()+":"+i); } } return null; } } public class ThreadPool { public static void main(String[] args) { //1.提供指定线程数量的线程池 ExecutorService service = Executors.newFixedThreadPool(10); ThreadPoolExecutor service1 = (ThreadPoolExecutor) service; //设置线程池的属性 System.out.println(service.getClass()); service1.setCorePoolSize(15); // service1.setKeepAliveTime(); //2.执行指定线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象 // service.execute(new NumberThread());//适合使用于Runnable NumberThread numberThread = new NumberThread(); service.submit(numberThread);//适合使用于Callable //3.关闭线程池 service.shutdown(); } }
小结释放锁的操作:
-
当前线程的同步方法、同步代码块执行结束
-
当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、方法的继续执行
-
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
-
当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
小结不会释放锁的操作:
-
线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
-
线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁
第九章、Java常用类
一、字符串相关的类 :String, StringBuffer、StringBuilder
1.String的特性:
-
String类:代表字符串。Java程序中的所有字符串字面值(如:“abc”)都作为此类的实例实现
-
String是一个final类,代表不可变的字符序列
-
字符串是常量,用双引号引起来表示。它们的值在创建之后不能修改
-
String对象的字符内容是存储在一个字符数组value[ ]中的
public class StringTest { /** * String:字符串 * 1.String声明为final的,不可继承 * 2.String实现了Serializable接口,表示字符串是支持序列化的 * 实现了Comparable接口,表示String是可以比较大小的 * 3.String内部定义了final char[] value,用于存储字符串数据。 * 4.String代表不可变的字符序列。简称:不可变性。 * 体现:1)当对字符串重新赋值时,需要重新指定内存区域赋值,不能使用原有的value进行赋值 * 2)当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值 * 3)当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值 * 5.通过字面量的方式(区别于new的方式)给一个字符串赋值, * 此时的字符串值声明在字符串常量池中。 * 6.字符串常量池中是不会存储相同内容的字符串的。 */ @Test public void test1(){ String s1 = "abc";//字面量的定义方式 String s2 = "abc"; s1 = "hello"; System.out.println(s1 == s2);//比较s1和s2的地址值 System.out.println(s1); System.out.println(s2); System.out.println("**************************"); String s3 = "abc"; s3 += "def"; System.out.println(s3);//abcdef System.out.println(s3 == s2);//false System.out.println("***********************"); String s4 = "abc"; String s5 = s4.replace('a','m'); System.out.println(s4);//abc System.out.println(s5);//mbc } }
2.String对象的创建:
//字面量的形式创建 String str = "hello"; //本质上this.value = new char[]; String s1 = new String(); //本质上this.value = original.value; String s2 = new String (String original); //本质上this.value = Arrays.copyOf(value , value.length); String s3 = new String(char []a); String s4 = new String (char []a,int startIndex,int count);
/** * String的实例化方式: * 方式一:通过字面量定义的方式 * 方式二:通过new+构造器的方式 */ @Test public void test2(){ //方式一:此时的s1和s2的数据javaEE声明在方法区中的字符串常量池中 String s1 = "javaEE"; String s2 = "javaEE"; //方式二:此时的s3和s4保存的地址值,是数据在堆空间中开辟空间以后对应的地址值。 String s3 = new String("javaEE"); String s4 = new String("javaEE"); // String s5 = s4; // System.out.println(s5 == s4); System.out.println(s1 == s2);//true System.out.println(s1 == s3);//false System.out.println(s1 == s4);//false System.out.println(s3 == s4);//false Person p1 = new Person("Tom",12); Person p2 = new Person("Tom",12); System.out.println(p1.name.equals(p2.name));//true System.out.println(p1.name == p2.name);//true p1.name = "jerry"; System.out.println(p2.name);//Tom } }
面试题:String s = new String ("abc");方式创建对象,在内存中创建了几个对象?
答:两个。一个是堆空间中new的结构,另一个是char[]对应的常量池中的数据:"abc"
3.字符串的拼接
@Test public void Test3(){ String s1 = "javaEE"; String s2 = "hadoop"; String s3 = "javaEEhadoop"; String s4 = "javaEE"+"hadoop"; String s5 = s1+"hadoop"; String s6 = "javaEE"+s2; String s7 = s1 + s2; String s8 = (s1 + "hadoop").intern();//返回值得到的s8使用常量池中已经存在的”javaEEhadoop“ System.out.println(s3 == s4);//true System.out.println(s3 == s5);//false System.out.println(s3 == s6);//false System.out.println(s3 == s7);//false System.out.println(s5 == s6);//false System.out.println(s5 == s7);//false System.out.println(s6 == s7);//false System.out.println(s4 == s8);//true /*结论:1.常量与常量的拼接结果在常量池。且常量池中不会存在相同内容的常量 2.只要其中有一个是变量。结果就在堆中。 3.intern()强制把字符串转为常量池中的 */ }
一道关于字符串值传递的面试题
4.String类中的方法
length(),charAt(),isEmpty(),toLowerCase(),toUpperCase(),trim(),equals(),equalsIgnoreCase(),concat(),compareTo(),substring()
注意点:
-
不变性:调用方法的字符串本身不会发生变化,返回值需要一个新的字符串来接收
-
trim()对字符串中间的空白不造成影响
-
equals()比较的是 是否 相等,返回值为true/false,compareTo比较的是大小,返回的是具体的值,比如"a".compareTo("c")为-2
-
substring(int beginIndex,int endIndex),区间为左闭右开,后面大部分都是这样,但是值得注意有的方法第二个参数的意义是取多少个
什么情况下 indexOf(str) 和 lastIndexOf(str) 的返回值相同?
要么str只出现了一次,要么str不存在于该字符串中
替换、匹配、切片
-
replace() 替换的是字符串中所有出现的。既可以替换字符,又可以替换字符串
-
replaceAll()和replaceFirst() 的第一个参数是正则表达式,根据正则表达式替换成第二个参数
-
matches() 表示是否匹配,如"123".matches("\ \ d+") 返回为true
-
split()的返回值为字符串数组,以正则表达式来分开字符串并将每部分放入数组中
5.String与包装类和基本数据类型的转换
6.String与字符数组的转换:String和char[ ]
public class StringTest2 { /* String--> char[] :调用String 的 toCharArray()方法 char[] -->String :调用String 的构造器 String(char[] chars) 和String(char[] ch,int startIndex,int cnt) */ public static void main(String[] args) { String s1 = new String("123"); char[] ch = s1.toCharArray(); for (int i = 0; i < ch.length; i++) { System.out.println(ch[i]); } String s2 = new String(ch); System.out.println(s2); char [] ch1 = new char[]{'h','3','e','l','l','o'}; String s3 = new String(ch1,1,4); System.out.println(s3); } }
7.StringY与字节数组的转换:String 和 byte[]
/* String与byte[]的转换 编码:String -->byte[]:调用String 的getBytes()方法 解码:byte[]-->String:调用String的构造器 编码:字符串-->字节,(看的懂-->看不懂) 解码:字节-->字符串,(看不懂-->看得懂) 说明:在解码时,要求解码使用的字符集必须与编码时使用的字符集一致,否则会出现乱码 */ @Test public void test1 () throws UnsupportedEncodingException { String s1 = "1A34中国"; byte[] bytes = s1.getBytes(); //使用默认的字符集进行b编码。 System.out.println(Arrays.toString(bytes)); byte[] gbks = s1.getBytes("gbk");//使用gbk字符进行编码 System.out.println(Arrays.toString(gbks)); String s2 = new String(bytes);//使用默认的字符集进行解码 System.out.println(s2); String s3 = new String(gbks);使用默认的字符集进行gbk字符解码 System.out.println(s3);//出现乱码。原因:编码集和解码集不一致 String s4 = new String(gbks, "gbk"); System.out.println(s4);//没有出现乱码,原因:编码集和解码集是一致的 }
解决一个拼接问题:
StringBuffer和StringBuilder
String、StringBuffer、StringBuilder的异同: String:不可变的字符序列;底层使用char[]value存储(String每次改变都要新建,效率最低) StringBuffer:可变的字符序列;线程安全的,效率低;底层使用char[]value存储 StringBuilder:可变的字符序列;jdk 5.0 新增的线程不安全的,效率高;底层使用char[]value存储 源码分析: String str = new String();//char[] value = new char[0]; String str1 = new String("abc");//char[] value = new char[]{'a','b','c'}; StringBuffer sb1 = new StringBuffer();//char[] value = new char[16];底层创建了一个长度是16的数组 sb1.append('a');//value[0] = 'a'; sb1.append('b');//value[1] = 'b'; StringBuffer sb2 = new StringBuffer("abc");//char[] value = new char["abc".length+ 16]; //问题1.System.out.println(sb2.length()); 答:3 //问题2.扩容问题:如果要添加的数据底层数组盛不下了,那就需要扩容底层的数组。 答:默认情况下,扩容为原来容量的两倍+2,同时将原有数组中的元素复制到新的数组中。 指导意义:开发中建议大家使用:StringBuffer(int capacity) 或 StringBuilder(int capacity) 因为这样可以有效避免扩容和复制,效率比较高
StringBuffer类的常用方法:
其中,xxx支持多种类型的数据
方法链的原理:由于append()等方法的返回值是修改后的字符串,因此str.append('a').append('b')这样的操作是可行的
总结:
-
增append()
-
删delete(int start,int end)
-
改replace(int start,int end , String str) / replace(int start,int end , char ch)
-
查 charAt(int n)
-
插 insert(int n ,xxx)
-
长度 length()
-
遍历 for + charAt() / toString()
对比三者的效率:
从高到低排列:StringBuilder > StringBuffer > String
String 、StringBuffer、StringBuilder的转换
String--->StringBuffer、StringBuilder: 调用StringBuffer和StringBuilder的构造器:new StringBuffer(String str)
StringBuffer、StringBuilder--->String: 调用String的构造器:new String(StringBuffer s) ; 或者 调用StringBuffer的toString()方法,返回的是一个String类型变量
一个易错题
@Test //如何解决?查看append()源码和StringBuffer()构造器源码 public void test(){ String str = null; StringBuffer sb = new StringBuffer(); sb.append(str); System.out.println(sb.length());//4 System.out.println(sb);//"null" StringBuffer sb1 = new StringBuffer(str);//抛出异常 System.out.println(sb1); }
二、JDK8之前日期时间API
1.java.lang.System类
System类提供的public static long currentTimeMillis()用来返回当前时间与1970年1月1日0时0分0秒之间以毫秒为单位的时间差,这样一个时间被称为时间戳
2.java.util.Date类
-
该类有个子类 :java.sql.Date类
(1) 两个构造器的使用
-
空参:Date():创建一个当前时间的Date对象
-
Date(long time):创建指定毫秒数的Date对象
(2)两个方法的使用
-
toString():显示当前的年、月、日、时分秒(toString()一般都是输出一些格式化的东西)
-
getTime():获取当前Date对象对应的毫秒数。(时间戳),相当于System类里的currentTimeMillis()方法
(3)java.sql.Date 对应着数据库中的日期类型的变量
-
如何实例化?new java.sql.Date(long time);
-
java.sql.Date 如何转为java.util.Date对象?多态,直接赋值
-
如何将java.util.Date 如何转为java.sql.Date对象?
-
情况1:
java.util.Date date = new java.sql.Date(); java.sql.Date date1 = (java.sql.Date) date;
-
情况2:
java.util.Date date = new java.util.Date(); java.sql.Date date1 = new java.sql.Date(date.getTime());
-
3.java.text.SimpleDateFormat类
public class DateTimeTest { /* SimpleDateFormat的使用:SimpleDateFormat对日期Date类的格式化和解析 1.两个操作: 1.1格式化: 日期-->字符串 1.2解析 : 格式化的逆过程 ,字符串-->日期 2.SimpleDateFormat的实例化 */ @Test public void test() throws ParseException { //实例化SimpleDateFormat:使用默认的构造器 SimpleDateFormat sdf = new SimpleDateFormat(); //格式化:日期-->字符串 Date date = new Date(); System.out.println(date); String format = sdf.format(date); System.out.println(format); //解析:字符串-->日期 String str = "2022/1/26 下午1:45";//字符串必须满足默认这样的格式, // 不然解析不了,这样太局限了,所以我们使用带有参数的SimpleDateFormat构造器 Date date1 = sdf.parse(str); System.out.println(date1); //***********按照指定的方式进行格式化和解析:使用带参的构造器******************** //实例化SimpleDateFormat:使用带参的构造器 SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); //格式化 Date date2 = new Date(); String format1 = sdf1.format(date2); System.out.println(format1); //解析:要求字符串必须是符合SimpleDateFormat识别的格式(通过构造器参数体现), //否则,抛出ParseException异常 String s1 = "2021-12-12 19:34:35"; Date date3 = sdf1.parse(s1); System.out.println(date3); } }
/* 练习1:字符串”2020-09-08“转换为java.sql.Date类 */ @Test public void exer(){ SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); Date date = null; try { date = simpleDateFormat.parse("2020-09-08"); } catch (ParseException e) { e.printStackTrace(); } java.sql.Date date1 = new java.sql.Date(date.getTime()); System.out.println(date1); }
4.java.util.Calendar(日历)类
-
该类为抽象类
小tips:注意区分 类名.class 和 对象.getclass()
/* Calendar日历类的使用 */ @Test public void test1(){ //1.实例化类 //方式1:创建子类(GregorianCalendar)的对象 //方式2:调用其静态方法getInstance() Calendar calendar = Calendar.getInstance(); System.out.println(calendar.getClass());//class java.util.GregorianCalendar //2.常用方法 //get() int days = calendar.get(Calendar.DAY_OF_MONTH); System.out.println(days); int day01 = calendar.get(Calendar.DAY_OF_YEAR); System.out.println(day01); //set() //这里体现了Calendar类的可变性 calendar.set(Calendar.DAY_OF_MONTH, 22 ); int days1 = calendar.get(Calendar.DAY_OF_MONTH); System.out.println(days1); //add() calendar.add(Calendar.DAY_OF_MONTH,-3); System.out.println(calendar.get(Calendar.DAY_OF_MONTH)); //getTime():日历类--->Date类 Date date = calendar.getTime(); // System.out.println(date); //setTime():Date类-->日历类 Date date1 = new Date(); calendar.setTime(date1); int dayy = calendar.get(Calendar.DAY_OF_MONTH); System.out.println(dayy); }
三、JDK8中新日期时间API
1.LocalDate/LocalTime/LocalDateTime类的使用 -->类似于Calendar类
/** * jdk8中日期时间API的测试 * @author Sheep * @create 2022/1/26 */ public class JDK8DateTimeTest { @Test public void test(){ //偏移量 Date date = new Date(2022-1900,1-1,2);// System.out.println(date);//Sun Jan 02 00:00:00 CST 2022 } /* LocalDate、LocalTime、LocalDateTime的使用 说明: 1.LocalDateTime相较于LocalTime、LocalDate,使用频率要高 2.类似于Calendar */ @Test public void test1(){ //now():获取当前的日期、时间、日期+时间。 LocalDate localDate = LocalDate.now(); LocalTime localTime = LocalTime.now(); LocalDateTime localDateTime = LocalDateTime.now(); System.out.println(localDate); System.out.println(localTime); System.out.println(localDateTime); //of( ):设置指定的年月日时分秒。没有偏移量 LocalDateTime localDateTime1 = LocalDateTime.of(2020, 10, 6, 19, 20, 12); System.out.println(localDateTime1); System.out.println("******************************"); //getXxx():获取相关属性 int dayOfMonth = localDateTime.getDayOfMonth(); System.out.println(dayOfMonth); System.out.println(localDateTime.getDayOfWeek()); System.out.println(localDateTime.getMonth()); System.out.println(localDateTime.getMonthValue()); System.out.println(localDateTime.getMinute()); //withXxx():设置相关属性 // LocalDate/LocalTime/LocalDateTime具有不可变性,修改值时本身不变,返回修改后的对象 LocalDate localDate1 = localDate.withDayOfMonth(26); System.out.println(localDate); System.out.println(localDate1); LocalDateTime localDateTime2 = localDateTime.withHour(10); System.out.println(localDateTime); System.out.println(localDateTime2); //plusXxx():加 //也体现了不可变性 LocalDateTime localDateTime3 = localDateTime.plusDays(1); System.out.println(localDateTime); System.out.println(localDateTime3); //minusXxx(): LocalDateTime localDateTime4 = localDateTime.minusMonths(2); System.out.println(localDateTime); System.out.println(localDateTime4); } }
2.瞬时:Instant -->类似于Date类
/* Instant的使用 类似于java.util.Date类 */ @Test public void test2(){ //实例化Instant类方法1:now():获取本初子午线对应的标准时间-->Date() Instant instant = Instant.now(); System.out.println(instant);//现在是15点,但是输出2022-01-27T07:09:51.402297500Z //原因在于时区不同 //利用atOffSet()方法添加偏移量 OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8)); System.out.println(offsetDateTime);//2022-01-27T15:09:51.402297500+08:00 //toEpochMilli():获取自1970年1月1日0时0分0秒(UTC)开始的毫秒数 long milli = instant.toEpochMilli(); System.out.println(milli); //实例化Instant类方法2:ofEpochMilli():通过给定的毫秒数,获取Instant实例-->Date(long millis) Instant instant1 = Instant.ofEpochMilli(1643267546120l); System.out.println(instant1); }
3.格式化与解析日期或时间
java.time.format.DateTimeFormatter类-->类似于SimpleDateFormat
/* DateTimeFormatter:格式化或解析日期、时间 */ @Test public void test3(){ //实例化有三种方式,前两种了解一下即可 //方式一:预定义的标准格式。如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; //格式化:日期-->字符串 LocalDateTime localDateTime = LocalDateTime.now(); String format = formatter.format(localDateTime); System.out.println(localDateTime); System.out.println(format); //解析:字符串-->日期 String str = "2022-01-27T15:32:26.7801679"; TemporalAccessor parse = formatter.parse(str); System.out.println(parse); //方式二: // 1)本地化相关的格式。如:ofLocalizedDateTime() //FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT : 适用于LocalDateTime ,参数不同输出的格式不同 DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM); //格式化:format LocalDateTime localDateTime1 = LocalDateTime.now(); String str1 = dateTimeFormatter.format(localDateTime1); System.out.println(str1);//2022年1月27日 下午3:42:40 //解析:parse //2)本地化相关的格式。如:ofLocalizedDate() //FormatStyle.FULL / FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT :适用于LocalDate DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL); //格式化 LocalDate localDate = LocalDate.now(); String format1 = dateTimeFormatter1.format(localDate); System.out.println(format1);//2022年1月27日星期四 //方式三:自定义的格式。如:ofPattern("yyyy-MM-dd hh:mm:ss") DateTimeFormatter dateTimeFormatter2 = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"); //格式化 String format2 = dateTimeFormatter2.format(LocalDateTime.now()); System.out.println(format2);//2022-01-27 03:54:29 //解析 TemporalAccessor parse1 = dateTimeFormatter2.parse("2022-01-28 00:00:01"); System.out.println(parse1); }
四、Java比较器
1.Comparable接口的使用举例: 自然排序
-
.像String、包装类等实现了Comparable接口,重写了compareTo()方法,给出了比较两个对象大小的方式
-
.像String、包装类重写compareTo()方法以后,进行了从小到大的排列
-
.重写compareTo()的规则: 如果当前对象this小于形参对象obj,则返回正整数, 如果当前对象this大于形参对象obj,则返回负整数, 如果当前对象this等于形参对象obj,则返回0
-
对于自定义类来说,如果需要排序,我们可以让自定义类实现Comparable接口,重写compare To(obj)方法,在compareTo(obj)方法中指明如何排序。
package com.cg.java; /** * @author Sheep * @create 2022/1/27 */ public class Goods implements Comparable{ private String name; private double price; public Goods(String name, double price) { this.name = name; this.price = price; } public Goods() { } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } @Override public String toString() { return "Goods{" + "name='" + name + '\'' + ", price=" + price + '}'; } //指明商品比较大小的方式:按照价格从低到高排序,再按照产品名称从低到高排序 @Override public int compareTo(Object o) { if(o instanceof Goods){ Goods good = (Goods) o; if (this.price > good.getPrice()){ return 1; }else if (this.price < good.price){ return -1; }else{ //return 0; return this.name.compareTo(good.name); } //return Double.compare(this.price , good.price) } throw new RuntimeException("传入的数据类型不一致"); } }
@Test public void test1(){ Goods[] arr = new Goods[4]; arr[0] = new Goods("lianxiang",34); arr[1] = new Goods("dellMouse",43); arr[2] = new Goods("xiaomiMouse",26); arr[3] = new Goods("huaweiMouse",12); Arrays.sort(arr); System.out.println(Arrays.toString(arr)); }
-
Comparator接口
/* Comparator接口的使用:定制排序 1.背景: 当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码 或者 实现了Comparable接口但是排序规则不适合当前的操作 2.重写compare(Object o1,Object o2)方法,比较大小。 */ @Test public void test2(){ String[] strings = new String[]{"aa","gg","mm","cc"}; Arrays.sort(strings, new Comparator() {//匿名子类 //按照字符串从大到小排列 @Override public int compare(Object o1, Object o2) { if (o1 instanceof String && o2 instanceof String){ String s1 = (String) o1; String s2 = (String) o2; return -s1.compareTo(s2); } return 0; } }); System.out.println(Arrays.toString(strings)); } @Test public void test4(){ Goods[] arr = new Goods[4]; arr[0] = new Goods("lianxiang",34); arr[1] = new Goods("dellMouse",43); arr[2] = new Goods("xiaomiMouse",26); arr[3] = new Goods("huaweiMouse",12); Arrays.sort(arr, new Comparator<Goods>() { @Override public int compare(Goods o1, Goods o2) { if (o1.getName().equals(o2.getName())){ return -Double.compare(o1.getPrice(),o2.getPrice()); }else { return o1.getName().compareTo(o2.getName()); } } }); System.out.println(Arrays.toString(arr)); }
3.Comparable接口与Comparator的使用的对比:
-
Comparable接口的方式一旦一定,保证Comparable接口实现类的对象在任何位置都可以比较大小
-
Comparator接口属于临时性的比较。
-
Comparable是实现在需要被排序的类中的,而Comparator接口是实现在排序过程中的
-
Comparable接口中的方法是compareTo(Object o),Comparator接口中的方法是compare(Object o1,Object o2)
五、三个类
1.System类:
-
3个属性:err,in,out
-
4个方法:currentTimeMillis(),exit(),gc(),getProperty()
2.Math类
3.BigInteger、BigDecimal
第十章、枚举类&注解
一、枚举类的使用
主要内容:
-
如何自定义枚举类
-
如何使用关键字enum定义枚举类
-
Enum类的主要方法
-
实现接口的枚举类
1.枚举类的使用:入门
-
枚举类的理解:==类的对象==只有有限个,确定的
-
当需要定义一组常量时,强烈建议使用枚举类
-
如果枚举类中只有一个对象,则可以作为一种单例模式的实现方式
2.如何定义枚举类
方式一:jdk5.0之前,自定义枚举类
//复习一个小知识,final 的属性可以赋值的地方有:显式赋值,代码块赋值,构造器赋值,其中显式赋值和代码块赋值得到的类的多个对象的这些属性的值都是一致的,而构造器赋值可以给不同对象赋上不同的属性值。
//自定义枚举类 class Season{ //1.声明Season对象的属性:private final 修饰 private final String seasonName ; private final String seasonDesc ; //2.私有化类的构造器 private Season(String seasonName , String seasonDesc){ this.seasonName = seasonName; this.seasonDesc = seasonDesc; } //3.提供当前枚举类的多个对象: public static final 修饰 public static final Season SPRING = new Season("春天","春暖花开"); public static final Season SUMMER = new Season("夏天","夏日炎炎"); public static final Season AUTUMN = new Season("秋天","秋高气爽"); public static final Season WINTER = new Season("冬天","冰天雪地"); //4.其他诉求1:获取枚举类对象的属性 public String getSeasonName() { return seasonName; } public String getSeasonDesc() { return seasonDesc; } //其他诉求2.toString() @Override public String toString() { return this.seasonName; } }
方式二:jdk5.0之后,可以使用enum关键字定义枚举类
//用enum关键字实现枚举类 //说明:定义的枚举类默认继承于java.lang.Enum类 enum Season1{ //3.提供当前枚举类的多个对象,多个对象之间用”,“隔开,末尾对象”;“结束 SPRING("春天","春暖花开"), SUMMER("夏天","夏日炎炎"), AUTUMN("秋天","秋高气爽"), WINTER("冬天","冰天雪地"); //2.声明Season对象的属性:private final 修饰 private final String seasonName ; private final String seasonDesc ; //3.私有化类的构造器 private Season1(String seasonName , String seasonDesc){ this.seasonName = seasonName; this.seasonDesc = seasonDesc; } //4.其他诉求1:获取枚举类对象的属性 public String getSeasonName() { return seasonName; } public String getSeasonDesc() { return seasonDesc; } //使用enum定义的枚举类继承于Enum类,而Enum类改写了toString()方法,如果我们有自己的诉求再去重写 // //其他诉求2.toString() // // @Override // public String toString() { // return this.seasonName; // } }
3.Enum类中的常用方法
public static void main(String[] args) { Season1 summer = Season1.SUMMER; //toString(): 实例方法 System.out.println(summer);//SUMMER System.out.println(Season1.class.getSuperclass());//java.lang.Enum //values(): 静态方法 Season1[] values = Season1.values(); for (int i = 0; i < values.length; i++) { System.out.println(values[i]); } //valueOf(String objName):返回枚举类中对象名是objName的对象。 静态方法 //如果没有objName的枚举类对象,则抛异常;IllegalArgumentException Season1 winter = Season1.valueOf("WINTER"); System.out.println(winter); }
4.使用enum关键字定义的枚举类实现接口的情况
(1)情况一:实现接口,在enum类中实现抽象方法
interface Info{ void show(); } //用enum关键字实现枚举类 //说明:定义的枚举类默认继承于java.lang.Enum类 enum Season1 implements Info{ //3.提供当前枚举类的多个对象,多个对象之间用”,“隔开,末尾对象”;“结束 SPRING("春天","春暖花开"), SUMMER("夏天","夏日炎炎"), AUTUMN("秋天","秋高气爽"), WINTER("冬天","冰天雪地"); @Override public void show() { System.out.println("这是一个季节"); } }
(2)情况二:让枚举类的对象分别实现接口中的抽象方法
interface Info{ void show(); } //用enum关键字实现枚举类 //说明:定义的枚举类默认继承于java.lang.Enum类 enum Season1 implements Info{ //3.提供当前枚举类的多个对象,多个对象之间用”,“隔开,末尾对象”;“结束 SPRING("春天","春暖花开"){ @Override public void show() { System.out.println("春眠不觉晓"); } }, SUMMER("夏天","夏日炎炎"){ @Override public void show() { System.out.println("夏天过去了"); } }, AUTUMN("秋天","秋高气爽"){ @Override public void show() { System.out.println("秋天在哪里"); } }, WINTER("冬天","冰天雪地"){ @Override public void show() { System.out.println("冬天真的冷啊"); } }; }
二、注解(Annotation)
1.注解的使用
( 1). 注解概述
(2)常见的Annotation示例
-
使用Annotation时要在其前面加上@符号,并把该Annotation当成一个修饰符使用。用于修饰它支持的程序元素
(3)如何自定义注解:参照@SuppressWarnings定义
-
注解声明为@interface
-
内部定义成员,通常使用value表示
-
可以指定成员的默认值,使用default定义
-
如果自定义注解没有成员,表明是一个标识作用
如果注解有成员,在使用注解时,需要指明成员的值(如果有默认值,可以不显式重新声明值)
自定义注解必须配上注解的信息处理流程(使用反射)才有意义。
自定义注解一般都会指定两个元注解:Retention、Target
(4)jdk提供的4种元注解
元数据 : 修饰其他数据的数据
元注解:用于修饰其他注解的注解
-
Retention : 指定所修饰的Annotation的生命周期:SOURCE/CLASS(默认行为)/RUNTIME
只有声明为RUNTIME生命周期的注解,才能通过反射获取。
-
Target:用于指定被修饰的Annotation能用于修饰哪些程序元素
下面出现的两个元注解使用频率较低
-
Documented:表示所修饰的注解在被javadoc解析时,保留下来。默认情况下解析成文档时,注解是不会被保留的。
-
Inherited:被它修饰的注解将具有继承性
(5)通过反射获取注解信息--->到反射章节再细节讲解
(6)jdk8 中注解的新特性:可重复注解、类型注解
-
可重复注解
-
jdk8之前的写法:
//@MyAnnotation("hello") //@MyAnnotation("abc") //jdk8 之前的可重复注解的写法 @MyAnnotations({@MyAnnotation("hello"),@MyAnnotation("abc")}) class Person{ void run(){ } } @interface MyAnnotations { MyAnnotation[] value(); } @interface MyAnnotation { String value() default "he"; }
-
-
-
jdk8以后的写法:
(1)声明一个新的自定义注解MyAnnotations,其成员为MyAnnotation数组
(2)在MyAnnotation上声明@Repeatable,成员值为MyAnnotations.class
(3) MyAnnotation的其他元注解要保持和MyAnnotations的相同
@Inherited @Repeatable(MyAnnotations.class) @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE}) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String value() default "he"; } @Retention(RetentionPolicy.RUNTIME) @Inherited @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE}) public @interface MyAnnotations { MyAnnotation[] value(); } //jdk8 之后的可重复注解的写法 @MyAnnotation("hello") @MyAnnotation("abc") class Person{ void run(){ } }
-
-
类型注解
ElementType.TYPE_PARAMETER (ElementType是一个枚举类,Target注解的成员就是ElementType数组)表示该注解能写在类型变量的声明语句中(如:泛型声明)。
ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中
第十一章、Java集合
一、集合框架的概述
1.集合、数组都是对多个数据进行存储操作的结构,简称java容器。
说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt, .jpg , .avi,数据库中)
2.1数组在存储多个数据方面的特点:
-
一旦初始化以后,其长度就确定了。
-
数组一旦定义好,其元素的类型也就确定了。我们只能操作指定类型的数据了。比如:String[ ] arr ; int [ ] arr1; Object [ ] arr2;
2.2数组在存储多个数据方面的缺点:
-
一旦初始化以后,其长度就不可修改。
-
数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。
-
获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用。
-
数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。
二、集合框架(Collection接口和map接口)
-
Collection接口:单列集合,用来存储一个一个的对象
-
List接口 : 存储有序的,可重复的数据。 常称为”动态数组“
-
ArrayList、LinkedList、Vector
-
-
Set接口 : 存储无序的,不可重复的数据。类似于数学中的集合
-
HashSet、LinkedHashSet、TreeSet
-
-
-
Map接口:双列集合,用来存储一对一对的数据(键值对)(key---value) 。类似于y=f(x) , value = f(key)
-
HashMap、LinkedHashMap、TreeMap、Hashtable、Properties.
-
三、Collection接口中的方法的使用
* 三、Collection接口中的方法的使用 * 要求: * 1.向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals() * 因为Collection中很多方法需要用到数据obj的equals()方法 */ public class CollectionTest { @Test public void test(){ Collection coll = new ArrayList(); //ArrayList是一个实现类,接口的抽象方法没有方法体 //1 add(Object e): 将元素e添加到集合coll中 coll.add("aa"); coll.add(123);//自动装箱 coll.add("123"); coll.add(new Date()); //2 size():获取添加的元素的个数 System.out.println(coll.size());//4 //3 addAll(Collection coll1): 将coll1集合中的元素添加到当前的集合中。 Collection coll1 = new ArrayList(); coll1.add(456); coll1.add("cc"); coll.addAll(coll1); System.out.println(coll.size());//6 System.out.println(coll);//[aa, 123, 123, Thu Feb 10 13:29:38 CST 2022, 456, cc] //4 clear(): //清空集合元素 // coll.clear();//清空元素 不是令coll = null; //5 isEmpty(): 判断当前集合是否为空 --->isEmpty()的源码就是 size()==0 // System.out.println(coll.isEmpty()); //6 contains(Object obj):判断当前集合中是否包含obj coll.add(new Person("Tom",20)); System.out.println(coll.contains(new String("123")));//true System.out.println(coll.contains(new Person("Tom",20)));//false :原因,没重写equals() //true: 原因,重写了equals() //7 containsAll(Collection coll2): //判断形参coll2中的所有元素是否都存在于当前集合中。 Collection coll2 = Arrays.asList(123,"123",new Person("Tom",20)); System.out.println(coll.containsAll(coll2)); } @Test public void test1(){ //8 remove(Object obj): 从当前集合中移除obj元素。 Collection coll = new ArrayList(); coll.add("aa"); coll.add(123);//自动装箱 coll.add("123"); coll.add("123"); coll.add(new Date()); coll.add(new Person("Tom",20)); coll.remove("aa"); coll.remove(new Person("Tom",20)); System.out.println(coll); //9 removeAll(Collection coll3):从当前集合中移除coll3中所有的元素。 // 将coll集合修改为coll对coll3的差集。 Collection coll3 = Arrays.asList(123,"123",new Person("Tom",20),12,53,64); coll.removeAll(coll3); System.out.println(coll); } @Test public void test2(){ Collection coll = new ArrayList(); coll.add("aa"); coll.add(123);//自动装箱 coll.add("123"); coll.add("123"); coll.add(new Date()); coll.add(new Person("Tom",20)); //10 retainAll(Collection coll3): 修改当前集合 为 当前集合和coll3集合的交集 Collection coll3 = Arrays.asList(123,"123",new Person("Tom",20),12,53,64); coll.retainAll(coll3); System.out.println(coll); //11 equals(Object obj): 要想返回true,需要当前集合和形参集合的元素都相同 Collection coll1 = new ArrayList(); coll1.add(123);//自动装箱 coll1.add("123"); coll1.add("123"); coll1.add(new Person("Tom",20)); System.out.println(coll.equals(coll1)); } @Test public void test3(){ Collection coll = new ArrayList(); coll.add("aa"); coll.add(123);//自动装箱 coll.add("123"); coll.add(new Date()); coll.add(new Person("Tom",20)); //12 hashCode(): 返回当前对象的哈希值 System.out.println(coll.hashCode()); //13 toArray() : 集合-->数组 Object[] objects = coll.toArray(); for (int i = 0; i < objects.length; i++) { System.out.println(objects[i]); } //拓展:Arrays.asList(T...a): 数组-->集合 List<String> list = Arrays.asList(new String[]{"aa", "bb", "cc"}); System.out.println(list);//[aa, bb, cc] //注:以下一个内容是易错点: List list1 = Arrays.asList(new int[]{123, 456}); System.out.println(list1);//[[I@1e643faf] System.out.println(list1.size());//1 List list2 = Arrays.asList(123, 456); System.out.println(list2);//[123, 456] System.out.println(list2.size());//2 List list3 = Arrays.asList(new Integer[]{123, 456}); System.out.println(list3);//[123, 456] System.out.println(list3.size());//2 //14 iterator(): 返回Iterator接口的实例,用于遍历集合元素。放在IteratorTest.java中测试 } }
* 集合元素的遍历操作,使用迭代器Iterator接口 1.内部的方法:hasNext() 和 next() * 2.集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前 * 3.内部定义了remove(),可以再遍历的时候,删除集合中的元素。此方法不同于集合直接调用remove(obj) */ public class IteratorTest { @Test public void test1(){ Collection coll = new ArrayList(); coll.add(123); coll.add("123"); coll.addAll(Arrays.asList(new Integer[]{456,789})); coll.add(LocalDateTime.now()); coll.add(new Person("Tom",20)); Iterator iterator = coll.iterator(); //方式一:next():不推荐 // System.out.println(iterator.getClass());//class java.util.ArrayList$Itr // System.out.println(iterator.next()); // System.out.println(iterator.next()); // System.out.println(iterator.next()); // System.out.println(iterator.next()); // System.out.println(iterator.next()); // System.out.println(iterator.next()); //如果next的个数超出了Collection集合中的元素个数,报NoSuchElementException //方式二:循环使用next() 也不推荐 // for (int i = 0; i < coll.size(); i++) { // System.out.println(iterator.next()); // } //方式三:推荐 while(iterator.hasNext()){ System.out.println(iterator.next()); } } @Test public void test2(){ //测试Iterator中的remove()方法 Collection coll = new ArrayList(); coll.add(123); coll.add("123"); coll.addAll(Arrays.asList(new Integer[]{456,789})); coll.add(LocalDateTime.now()); coll.add(new Person("Tom",20)); //删除集合中的”Tom“ Iterator iterator = coll.iterator(); while (iterator.hasNext()){ Object obj= iterator.next(); if ("123".equals(obj)){ iterator.remove(); } } //遍历 Iterator iterator1 = coll.iterator(); while(iterator1.hasNext()){ System.out.println(iterator1.next()); } System.out.println(coll); } }
==新特性foreach循环遍历集合元==
* jdk 5.0 新增了foreach循环,用于遍历集合、数组 * @author Sheep * @create 2022/2/12 */ public class ForTest { @Test public void test(){ Collection coll = new ArrayList(); coll.add(123); coll.add("123"); coll.addAll(Arrays.asList(new Integer[]{456,789})); coll.add(LocalDateTime.now()); coll.add(new Person("Tom",20)); //for(集合元素的类型 局部变量 : 集合对象) //内部仍然调用了迭代器 for(Object obj : coll){ System.out.println(obj); } //for(数组元素的类型 局部变量 : 数组对象) for (int i : new int[]{12,34,56}) { System.out.println(i); } }
//练习题 @Test public void test3(){ String[] arr = new String[]{"11","11","11"}; // for (int i = 0; i < arr.length; i++) { // arr[i] = "22"; // } for(String s : arr){ s = "22"; } for (String s : arr) { System.out.println(s); } }
四、Collection子接口之一:List接口
1.List接口框架
Collection接口:单列集合,用来存储一个一个的对象
List接口 : 存储有序的,可重复的数据。 常称为”动态数组“->用来替换原有的数组
ArrayList:作为List接口的主要实现类 ; 线程不安全,效率高;底层使用Object[ ]存储
LinkedList :对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储
Vector :作为List接口的古老实现类:线程安全,效率低;底层使用Object[ ]存储
Vector 从jdk1.0就已经出现,而List接口和ArrayList、LinkedList在jdk1.2才出现 jdk1.2之前,Vector作为一个有序、可重复的集合,jdk1.2之后,就把Vector归纳到List接口的实现类中
==面试题==:ArrayList 、 LinkedList 、 Vector的异同
同:三个类都实现了List接口,存储数据的特点相同:存储有序的、可重复的数据。
异:
-
ArrayList : 作为List接口的主要实现类 ; 线程不安全,效率高;底层使用Object[ ]存储;扩容时变成原来的1.5倍
-
LinkedList : 对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储
-
Vector : 作为List接口的古老实现类:线程安全,效率低;底层使用Object[ ]存储;扩容时变成原来的2倍
2.ArrayList的源码分析:jdk7和jdk8有所不同
(1)ArrayList的源码分析:jdk7的情况下:
ArrayList list = new ArrayList ();//底层创建了长度是10的Object[]数组elementData。 list.add(123);//element[0] = 123; ... list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容,默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组的数据复制到新的数组中。 结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity);//创建容量为capacity的集合
(2)ArrayList的源码分析:jdk8的情况下:
ArrayList list = new ArrayList();//底层Object[] elementData初始化为{}.并没有创建长度为10的数组. list.add(123);//第一次调用add()时,底层才创建了长度为10的数组,并将数组123添加到elementData中。 后续的添加和扩容操作与jdk 7无异
(3)小结:
jdk7中的ArrayList的对象的创建类似于单例模式中的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例模式的懒汉式,延迟了数组的创建,节省内存。
3.LinkedList的源码分析:
LinkedList list = new LinkedList();//内部声明了Node类型的first和last属性,默认值为null, list.add(123);//将123封装到Node中,创建了Node对象,然后添加进链表中。 其中,Node定义为:(体现了LinkedList的双向链表的特点) private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
4.Vector的源码分析:
jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组;在扩容方面,默认扩容为原来的数组长度的2倍
Vector效率低,古老,一般不用,那如果涉及到多个线程共享数据的情况,可以用Collections类中的SynchronizedList( List list)方法将ArrayList的一个实例对象作为参数传入,返回线程安全的List对象。
5.List接口中的常用方法
List接口中的方法 * void add(int index,Object ele):在index位置插入ele元素 * boolean addAll(int index,Collection eles):从index位置开始将eles中的所有元素添加进来 * Object get(int index):获取指定index位置的元素 * int indexOf(Object obj):返回obj在当前集合中首次出现的位置 * int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置 * Object remove(int index):移除指定index位置的元素,并返回此元素 * Object set(int index,Object ele):设置指定index位置的元素为ele * List subList(int fromIndex,int toIndex):返回从fromIndex到toIndex位置的子集合
@Test public void test1(){ ArrayList list = new ArrayList(); list.add(123); list.add(546); list.add(LocalDateTime.now()); list.add(123); System.out.println(list); //void add(int index,Object ele):在index位置插入ele元素 list.add(1,"AA"); System.out.println(list); //boolean addAll(int index,Collection eles):从index位置开始将eles中的所有元素添加进来 list.addAll(Arrays.asList(1,2,3)); System.out.println(list); //Object get(int index):获取指定index位置的元素 Object o = list.get(1); System.out.println(o); //int indexOf(Object obj):返回obj在当前集合中首次出现的位置 System.out.println(list.indexOf(123)); System.out.println(list.indexOf(Calendar.getInstance())); //int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置 System.out.println(list.lastIndexOf(123)); //Object remove(int index):移除指定index位置的元素,并返回此元素 Object obj = list.remove(3); System.out.println(list); System.out.println(obj); //Object set(int index,Object ele):设置指定index位置的元素为ele Object o1 = list.set(1, "BB"); System.out.println(list); System.out.println(o1); //List subList(int fromIndex,int toIndex):返回从fromIndex到toIndex位置的子集合 List list1 = list.subList(0, 2); System.out.println(list1); }
总结常用方法:
增:add(Object o)
删:remove(Object o) / remove(int index)
改:set(int index , Object o)
查:get(int index)
插:add(int index , Object o)
长度:size()
遍历: ①:Iterator迭代器方式 ②:foreach增强for循环 ③:普通的for循环 ④:Collection接口继承了Iterable接口,里面有forEach()方法,能很快遍历,代码在下面
@Test public void test2(){ ArrayList list = new ArrayList(); list.add(123); list.add(456); list.add("AA"); list.add(LocalDateTime.now()); //遍历方式1: Iterator iterator = list.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } System.out.println("***********"); //遍历方式2: for (Object o: list ) { System.out.println(o); } System.out.println("*****************"); //遍历方式3: for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } //遍历方式4: Collection collection = new ArrayList(); collection.add(123); collection.add(456); collection.add("123"); collection.add(24); collection.forEach(System.out::println); }
这里的remove(2) 是remove(int index)的调用
如果要调用remove(Object o),需要使用remove(new Integer(2))
五、Collection子接口之二:Set接口
1.Set接口的框架结构:(Set接口中没有额外定义新的方法,使用的都是Collection中声明过的方法)
Collection接口:单列集合,用来存储一个一个的对象
Set接口 : 存储无序的,不可重复的数据。类似于数学中的集合
HashSet : 作为Set接口的主要实现类;线程不安全的;可以存储null值;
LinkedHashSet : 作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历
TreeSet : 要求元素必须是同一个类的对象实例;可以按照添加对象的指定属性进行排序
2.
(1)Set:存储无序的、不可重复的数据
以HashSet为例说明:
-
无序性:不等于随机性。存储的数据在底层数组中并非按照索引的顺序添加,而是根据数据的哈希值决定的。
-
不可重复性:保证添加的元素按照equals()判断时,不能返回true。即:相同的元素只能添加一次。
(2)添加元素的过程:
(要求:向Set中添加的数据,其所在的类一定要重写hashCode()和equals() )
(要求:重写equals()和hashCode()要尽量保持一致,即想等的对象必须具有相等的散列码)
以HashSet为例说明:
我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置),判断数组此位置上是否已经有元素:
-
如果此位置上没有其他元素,则元素a添加成功。--->情况1
-
如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a和这些元素的hash值:
-
如果hash值不相同,则元素a添加成功 ---->情况2
-
如果hash值相同,进而需要调用元素a所在类的equals()方法
-
equals()返回true,元素a添加失败
-
equals()返回false,元素a添加成功 ---->情况3
-
-
对于添加成功的情况2和情况3而言,元素a与已经存在指定索引位置上数据以链表的方式存储:
jdk7:元素a放到数组中,指向原来的元素
jdk8:原来的元素在数组中,指向元素a
总结:七上八下
HashSet底层:数组+链表的结构。
HashSet的默认数组长度为16,jdk7以前是创建HashSet时,就已经创建好数组,jdk8以后是add时才创建数组(此特性类似于ArrayList)
补充:在Object类中的hashCode()方法返回的是一个随机数用作存储数据的地址,而重写hashCode()的作用是将元素的属性通过一些计算得到一个hash值,用此hash值去决定在HashSet底层数组的存放位置,如果某两个元素的属性相同,hash值一定相同,但hash值相同,不一定元素的属性就相同(存在冲突)。
所以,向Set中添加的数据,其所在的类一定要重写hashCode()和equals()
3.LinkedHashSet的使用:
LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据的前一个数据和后一个数据。
优点:对于频繁的遍历操作,LinkedHashSet效率高于HashSet
4.TreeSet的使用
(1)向TreeSet中添加的数据,要求是相同类的对象。因为TreeSet底层是红黑树,大的在一边,小的在一边,需要可以比较,所以TreeSet中添加的数据要么得实现Comparable接口,要么创建TreeSet集合时指定Comparator接口。
(2)两种排序方式:自然排序和定制排序
(3)自然排序中,比较两个对象是否相同的标准为:compareTo()返回0,不再是equals().
也就是说,如果两个元素compareTo()比较之后得到的为0,那么认为他们相同,不能同时出现在TreeSet中。
(4)自然排序中,比较两个对象是否相同的标准为:compareTo()返回0,不再是equals().
(5)TreeSet的底层是红黑树,红黑树中不能存在相等的两个数。
@Test public void test1(){ TreeSet set = new TreeSet(); //失败:不能向TreeSet中添加不同类型的数据 // set.add(123); // set.add(456); // set.add("AA"); // set.add(new Person("Tom",20)); //举例1: // set.add(33); // set.add(-33); // set.add(23); // set.add(54); // set.add(1); // System.out.println(set);//[-33, 1, 23, 33, 54] //举例2:自然排序 //(这里的Person类继承了Comparable接口且已经重写了compareTo()方法) set.add(new Person("Tom",20)); set.add(new Person("Jerry",16)); set.add(new Person("hi",13)); set.add(new Person("sb",87)); set.add(new Person("name",20)); Iterator iterator = set.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } //举例3:定制排序 Comparator com = new Comparator(){ //按照年龄从小到大排列 @Override public int compare(Object o1,Object o2){ if (o1 instanceof Person && o2 instanceof Person){ Person p1 = (Person) o1; Person p2 = (Person) o2; return Integer.compare(p1.getAge(),p2.getAge()); }else{ throw new RuntimeException("不能比较"); } } }; TreeSet set1 = new TreeSet(com); set1.add(new Person("Tom",20)); set1.add(new Person("Jerry",16)); set1.add(new Person("hi",13)); set1.add(new Person("sb",87)); set1.add(new Person("name",20)); Iterator iterator1 = set1.iterator(); while (iterator1.hasNext()){ System.out.println(iterator1.next()); } }
List需要重写equals()方法,因为例如contains()、remove()这些方法需要调用equals()
Set:无序、不可重复的数据
-
HashSet和LinkedHashSet需要重写equals()和hashCode(),==因为需要用hash值来确定放在数组中的索引位置,再用hash值和equals()判断是否相等。==
-
==TreeSet 是利用compareTo()或compare()来判断是否相等的==。但是例如contains()、remove()这些方法需要调用equals(),所以也要重写。
综上:Collection集合存储的是自定义类的对象时就要重写equals()方法。
关于HashSet和List在判断相等上的区别:
-
List只能按照equals()逐个判断是否相等
-
HashSet先求hash值,再求equals()判断,其中remove(Object o)之类的方法也是先求出o的hash值,根据哈希值得到索引,再equals()判断是否相等,再删除。
一道易错题:
@Test public void test(){ HashSet set = new HashSet(); Person p1 = new Person("Tom", 20); Person p2 = new Person("Jerry", 19); set.add(p1); set.add(p2); System.out.println(set);//[Person{name='Tom', age=20}, Person{name='Jerry', age=19}] p1.age = 21; System.out.println(set);//[Person{name='Tom', age=21}, Person{name='Jerry', age=19}] set.remove(p1); System.out.println(set);//[Person{name='Tom', age=21}, Person{name='Jerry', age=19}] set.add(new Person("Tom",21)); System.out.println(set);//[Person{name='Tom', age=21}, Person{name='Tom', age=21}, Person{name='Jerry', age=19}] set.add(new Person("Tom",20)); System.out.println(set);//[Person{name='Tom', age=21}, Person{name='Tom', age=21}, Person{name='Tom', age=20}, Person{name='Jerry', age=19}] }
六、Map接口及其多个实现类
1.Map实现类的结构
Map:双列数据,存储key-value对的数据-->类似于高中的函数:y = f(x).
-
HashMap:作为Map的主要实现类;线程不安全的,效率高;可以存储null的key和value
-
LinkedHashMap:保证再遍历Map元素时,可以按照添加的顺序实现遍历
原因:再原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类执行效率高于HashMap
-
-
TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或者定制排序,底层使用红黑树
-
Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
-
Properties:常用来处理配置文件。key和value都是String类型
-
HashMap的底层:数组+链表(jdk7及以前)
数组+链表+红黑树(jdk8之后)
面试题:
(1).==HashMap的底层实现原理?==
(2).HashMap和Hashtable的异同?
(3).CurrentHashMap 和 Hashtable的异同?
2.Map结构的理解:
-
Map中的key:无序的,不可重复的,使用Set存储所有的key
-->key所在的类要重写equals()和hashCode() (以HashMap为例)
-
Map中的value:无序的,可重复的,使用Collection存储所有的value。
-->value所在的类要重写equals()
-
一个键值对:key-value构成了一个Entry对象。
-
Map中的entry:无序的、不可重复的,使用Set存储所有的entry。
3.==HashMap的底层实现原理?==
jdk7时:
HashMap map = new HashMap();
在实例化以后,底层创建了长度是16的一维数组Entry[] table.
...可能已经执行了多次put()... map.put(key1,value1);
首先,调用key1所在类的hashCode()(源码中是hashCode()被hash()方法调用,得到哈希值)计算key1哈希值,此哈希值经过某种算法计算之后,得到在Entry数组中的存放位置。
-
如果此位置上的数据为空,此时的key1-value1添加成功。---->情况1
-
如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
-
如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功 --->情况2
-
如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2) :
-
如果equals() 返回false:此时key1-value1添加成功 --->情况3
-
如果equals()返回true:使用value1替换value2
-
-
补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。
在不断的添加过程中,会涉及到扩容问题,当超出临界值(threshold)时(且要存放的位置非空时)需要扩容,默认的扩容方式:扩容为原来的2倍,并将原有的数据复制过来。
HashMap jdk8 相较于jdk7在底层实现方面的不同:
-
new HashMap(): 底层没有创建一个长度为16的数组,首次调用put() 方法时,底层创建长度为16的数组
-
jdk8底层的数组是Node[] ,而不是Entry []
-
jdk7 底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。
当数组的某一个索引位置上的元素以链表形式存在的数据个数>8 且当前数组的长度> 64时,此时此索引位置上的所有数据改为使用红黑树存储。
当数组的某一个索引位置上的元素以链表形式存在的数据个数>8 且当前数组的长度小于 64时,数组进行扩容。
-
形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
小补充:
几个集合 jdk7和jdk8一些区别:
-
HashSet 、HashMap 七上八下
-
ArrayList 调用构造器时底层没有创建一个长度为10的数组,首次调用add() 方法时,底层创建长度为10的数组
-
HashSet调用构造器时底层没有创建一个长度为16的数组,首次调用add()方法时,底层创建长度为16的数组
-
HashMap调用构造器时底层没有创建一个长度为16的数组,首次调用 put() 方法时,底层创建长度为16的数组
4.HashMap在jdk7时的源码分析:(多看几遍康师傅第552集源码分析)
手打源码:
//空参构造器: public HashMap(){ this(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);//调用HashMap的其他构造器,第一个参数为默认初始容量16,第二个参数为默认填充因子:0.75 } //带参的构造器 public HashMap(int initialCapacity,float loadFactor){ if (initialCapacity< 0){ throw new IllegalArgumentException("Illegal initial capacity:" + initialCapacity); } if (initialCapacity > MAXIMUM_CAPACITY){ initialCapacity = MAXIMUM_CAPACITY; //如果传入参数大于能满足的最大值,则以最大容量为该Map容量 } if (loadFactor <= 0 || Float.isNaN(loadFactor)){ throw new IllegalArgumentException("Illegal load factor:"+loadFactor); } //Find a power of 2>= initialCapacity //寻找一个大于等于传入参数的 2的幂 作为初始容量 int capacity = 1; //真正的初始容量 while (capacity < initialCapacity){ capacity <<= 1; //当capacity小于传入参数时,乘2 不断循环,直到capacity大于等于传入参数 } this.loadFactor = loadFactor; //填充因子 threshold = (int)Math.min(capacity* loadFactor,MAXIMUM_CAPACITY + 1); //将capacity*loadFactor作为扩容阈值 table = new Entry[capacity]; //创建容量为capacity的Entry类型数组用来存储数据 useAltHashing = sun.misc.VM.isBooted()&& (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); init(); } //put(): public V put(K key,V value){ if (key == null) return putForNullKey(value); //放置null值的操作,(Hashtable中没有处理null的方法) int hash = hash(key); //hash()内部调用了hashCode()方法计算出key的哈希值 int i = indexFor(hash,table.length); //hash和table.length在indexFor内部作了一次与运算,从而得出介于[0,table.length-1]的存放位置 for(Entry<K,V> e = table[i] ; e!= null ; e= e.next){ Object k; if (e.hash == hash && ((k=e.key) == key ||key.equals(k))){ V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue ; } } //从table[i]开始遍历处于数组中索引为i的 链表中的每个元素,如果有e.key值和key相等的,那么用新的value去替换e.value modCount++; addEntry(hash ,key ,value ,i); //如果没有找到相等的key,那么就将该键值对添加进去。 return null; } //addEntry(): void addEntry(int hash ,K key, V value, int bucketIndex){ if((size >= threshold) && (null != table[bucketIndex])){ resize(2* table.length); //如果HashMap元素个数大于阈值(且索引位置上不为空[已经有其他元素]),就需要扩容,扩容的方法是扩大到原容量的两倍。 hash = (null != key)? hash(key) : 0; //扩容之后,计算放置位置的算法变了,所以需要重新计算hash值并计算新的放置位置 bucketIndex = indexFor(hash,table.length); } //无论有没有扩容,接下来都要进行创建Entry并添加进去的操作: createEntry(hash,key,value,bucketIndex); } createEntry(int hash,K key ,V value , int bucketIndex){ Entry<K,V> e = table [bucketIndex]; //把在bucketIndex位置上原有的元素取出来给临时变量e table[bucketIndex] = new Entry<>(hash,key,valu,e); //创建一个新的Entry对象存储添加的键值对,并把该Entry对象的next赋值为e,再将该Entry对象放在bucketIndex位置上。 作用是将新加入的元素放在数组的bucketIndex位置上,并指向该位置上原来的那个元素。 size++; } //Entry的定义(内部类) static class Entry<K,V> implements Map.Entry<K,V>{ final K key; V value; Entry<K,V> next; int hash; //构造器 Entry(int h,K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } }
5.jdk8时HashMap的源码分析 :
DEFAULT_INITIAL_CAPACITY: HashMap的默认容量:16
DEFAULT_LOAD_FACTOR: HashMap的默认加载因子:0.75
threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12;
TREEIFY_THRESHOLD : Bucket中链表长度大于该默认值,转化为红黑树:8
MIN_TREEIFY_CAPACITY: 桶中的Node被树化时最小的hash表容量:64
手打源码:
//空参构造器: public HashMap(){ this.loadFactor = DEFAULT_LOAD_FACTOR;//加载因子赋值为默认值0.75 //我们发现这里没有调用别的构造器,说明创建HashMap时并未实现底层数组 } //put(): public V put (K key,V value){ return putVal( hash(key), key,value,false,true); //调用putVal方法,其中的后两个参数不用多关注 } //putVal(): final V putVal(int hash, K key,V value,boolean onlyIfAbsent,boolean evict){ Node<K,V>[] tab; Node<K,V> p ; int n,i; //创建了一些临时变量 if ((tab=table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 如果当前底层数组为空或者长度为0.则需要调用resize()方法创建数组。 if ((p = tab[i = (n-1)&hash]) == null) // 如果计算出的要放置的位置上原本没有数据,那么直接创建Node放进数组就行 tab[i] = newNode(hash,key,value,null); else{// 如果计算出的要放置的位置上已经有数据,进入该分支 Node<K,V> e ; K k; //新建临时变量 if (p.hash == hash && ((k = p.key) == key || (key !=null && key.equals(k)))) e = p; //如果该位置上原有的第一个数据和要加入的Node的key相等,那么将原有的这个数据赋值给临时变量e else if (p instanceof TreeNode) e = (TreeNode<K,V>p).putTreeVal(this,tab,hash,key,value) else{ for(int binCount = 0; ; ++binCount){ //无终止条件 if ((e = p.next) == null){ //如果p没有后继(遍历到最后一个了),开始添加 p.next = newNode(hash,key,value,null);//尾插 if(binCount >=TREEIFY_THRESHOLD - 1) treeifyBin(tab,hash);//如果遍历的时候发现链表长度超过了8,则调用treeifyBin(),可能会转为红黑树 break; } if (e.hash == hash && ((k = e.key)==key || (key!= null && key.equals(k)))) break; // 如果找到元素与要添加的key相等,那么停止循环,进入下一步替换 p = e; //将e(p.next) 赋给p,也就是考虑下一个元素 } } if (e != null){ // existing mapping for key //如果找到了和要添加的元素相同key的 原有元素,那么就进行替换 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } } //resize(): //当数组为空时,可以创建数组,容量不够时,可以扩容 final Node<K,V>[] resize(){ Node<K,V>[] oldTab = table; int oldCap = (oldTab == null)? 0 : oldTab.length; int oldThr = threshold; int newCap,newThr = 0; if (oldCap > 0){ if (oldCap >= MAXIMUM_CAPACITY){ threshold = Integer.MAX_VALUE; return oldTab; } else if((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; //double threshold } else if(oldThr > 0) //initial capacity was placed in threshold newCap = oldThr; else { newCap = DEFAULT_INITIAL_CAPACITY; //oldCap 和oldThr 都为0,因此进入该分支,赋值newCap和newThr分别为16,12; newThr = (int)(DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; Node<K,V> [] newTab = (Node<K,V>[]) new Node[newCap];//造新数组啦 table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } //treeifyBin(): final void treeifyBin(Node<K,V>[] tab,int hash){ int n,index ; Node<K,V> e;//定义临时变量 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize();//如果tab是空,那么调用resize()创建新的;如果tab的长度小于64,那么扩容。也就是说当链式结构的长度大于8,会启用treeifyBin(),此时,如果数组长度大于等于64,那么变成红黑树,如果小于64,扩容,这样可以减少树形结构的出现。 else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
StringBuffer和ArrayList都是到了当前容量值(16/10)才会扩容,而HashMap则是到了一定阈值就会扩容。
为什么HashMap 的数组不是满了才扩容呢?
因为HashMap的数组很难满,满了的时候可能会有很多链表存在了,这样不好,既要减少链表的存在,也要保证数组的利用率,就选择了0.75.
6.LinkedHashMap的底层实现原理(了解)
//LinkedHashMap重写了newNode()方法,在内部新建LinkedHashMap.Entry对象 Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<>(hash, key, value, e); linkNodeLast(p); return p; } //Entry是定义在LinkedHashMap中的静态内部类,里面定义了一前一后两个变量用来指向前一个和后一个结点。 static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
创建一个HashSet() 底层就创建一个HashMap(), HashSet()中的元素都作为key存放在Map中,而Map里的value是一个静态常量PRESENT (new Object()),即这些key都指向同一个,PRESENT没有实际意义
7.Map中定义的方法:
public class MapTest { @Test public void test(){ //put(Object o) Map map = new HashMap(); map.put("a", 1); map.put("b",2); map.put("c",3); map.put("d",4); map.put("d", 5);//修改value System.out.println(map); //putAll(Map map) Map map1 = new HashMap(); map1.put("e",6); map1.put("f",7); map.putAll(map1); System.out.println(map); //remove(Object o) Object value = map.remove("a"); System.out.println(value); System.out.println(map); //clear() map.clear(); //与map = null 不同 System.out.println(map.size());//0 } @Test public void test1(){ Map map = new HashMap(); map.put("a", 1); map.put("b",2); map.put("c",3); map.put("d",4); map.put("d", 5);//修改value //get(Object key) System.out.println(map.get("a")); //cotainsKey(Object key) boolean isExist = map.containsKey("a"); System.out.println(isExist); //containsValue(Object value) boolean isExist1 = map.containsValue(1); System.out.println(isExist1); //size() System.out.println(map.size()); //isEmpty() System.out.println(map.isEmpty()); map.clear(); System.out.println(map.isEmpty()); //equals(Object o):要想返回true,需要满足o也是一个map且存储的数据和map一致 Map map1 = new HashMap(); map1.put("a", 1); map1.put("b",2); Map map2 = new HashMap(); map2.put("b",2); map2.put("a",1); System.out.println(map1.equals(map2)); } @Test //由于Map没有实现Iterable接口,所以没有迭代器,也不能用foreach循环 //那么就要用下面3个方法把Map中的Key、Value、Entry提取出来作为Set或Collection集合遍历 public void test2(){ Map map = new HashMap(); map.put("he", 1); map.put("abc",2); map.put("hi",3); map.put("hello",4); //遍历所有的key集:keySet() Set set = map.keySet(); //得到key组成的Set集合,就可以用iterator集合了 Iterator iterator = set.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } //遍历所有的value集:values() Collection values = map.values(); for (Object o: values ) { System.out.println(o); } //遍历所有的key-value : // 方式1:entrySet() Set entrySet = map.entrySet(); Iterator iterator1 = entrySet.iterator(); while (iterator1.hasNext()){ Object obj = iterator1.next(); System.out.println(obj); // Map.Entry entry = (Map.Entry) obj; // System.out.println(entry.getKey() +"->"+entry.getValue()); } //方式二: Set set1 = map.keySet(); Iterator iterator2 = set1.iterator(); while (iterator2.hasNext()){ Object key = iterator2.next(); Object value = map.get(key); System.out.println(key+"-->"+value); } } }
总结:常用方法:
添加:Object put(Object key,Object value)
删除:Object remove(Object key): 返回value
修改:Object put(Object key,Object value)
查询:Object get(Object key): 返回value
长度:size()
遍历:Set keySet() ; Collection values() ; Set entrySet()
8.TreeMap介绍:
TreeMap是要排序的Map,所以添加的数据必须是同一类型,且要有Comparable或Comparator的实现
public class TreeMapTest { //向TreeMap中添加key-value,要求key必须是由同一个类创建的对象 //因为要按照key进行排序:自然排序、定制排序 @Test public void test1(){ TreeMap treeMap = new TreeMap(); treeMap.put(new Person(10,"Tom"),1); treeMap.put(new Person(15,"Jerry"),2); treeMap.put(new Person(4,"Smith"),3); treeMap.put(new Person(22,"go"),4); treeMap.put(new Person(26,"go"),4); treeMap.put(new Person(18,"go"),4); Set set = treeMap.keySet(); Iterator iterator = set.iterator(); while (iterator.hasNext()){ Object key = iterator.next(); System.out.println(key +"->"+ treeMap.get(key)); } } @Test public void test2(){ TreeMap treeMap = new TreeMap(new Comparator() { @Override public int compare(Object o1, Object o2) { if (o1 instanceof Person && o2 instanceof Person) { Person person1 = (Person) o1; Person person2 = (Person) o2; int compare = -person1.getName().compareTo(person2.getName()); if (compare == 0) { return -Integer.compare(person1.getAge(), person2.getAge()); } else { return compare; } } throw new RuntimeException("cannot be compared"); } }); treeMap.put(new Person(10,"Tom"),1); treeMap.put(new Person(15,"Jerry"),2); treeMap.put(new Person(4,"Smith"),3); treeMap.put(new Person(22,"go"),4); treeMap.put(new Person(26,"go"),4); treeMap.put(new Person(18,"go"),4); Set set = treeMap.keySet(); Iterator iterator = set.iterator(); while (iterator.hasNext()){ Object key = iterator.next(); Object value = treeMap.get(key); System.out.println(key +"-->" + value); } } }
9.Properties(由于Hashtable基本不用了,所以我们更关注它的子类)
public static void main(String[] args) { FileInputStream fis = null; try { Properties properties = new Properties(); fis = new FileInputStream("jdbc.properties");//其中jdbc.properties是一个配置文件 properties.load(fis); System.out.println(properties.getProperty("name")); System.out.println(properties.getProperty("age")); System.out.println(properties.getProperty("speed", "20")); } catch (IOException e) { e.printStackTrace(); } finally { if (fis != null){ try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } }
七、Collections工具类
-
Collections是一个操作Collection和Map等集合的工具类
面试题:Collection和Collections的区别:
public class CollectionsTest { @Test public void test(){ ArrayList list = new ArrayList(); list.add(12); list.add(65); list.add(23); list.add(85); list.add(76); list.add(124); System.out.println(list); //reverse(List):反转集合 Collections.reverse(list); System.out.println(list); //shuffle(List):随机排列 Collections.shuffle(list); System.out.println(list); Collections.shuffle(list); System.out.println(list); //sort(List):自然排序 Collections.sort(list); System.out.println(list); //sort(List,Comparator):定制排序 //swap(list,int , int):交换两索引位置上的值 Collections.swap(list,0,5); System.out.println(list); //max(Collection ):查找集合中最大最小值 //max(Collection ,Comparator) System.out.println(Collections.max(list)); //min(Collection) //min(Collection , Comparator) //frequency(Collection, Object):查看某元素出现次数 System.out.println(Collections.frequency(list, 12)); } @Test public void test1(){ //copy(List desc,List src) ArrayList list = new ArrayList(); list.add(12); list.add(65); list.add(23); list.add(85); list.add(76); // ArrayList list1 = new ArrayList(); // Collections.copy(list1,list);//报异常 //IndexOutOfBoundsException: Source does not fit in dest //这是因为copy()要求list1.size()大于list.size() //size():集合中元素的个数 //那要怎么撑起来list中相同个数的元素呢?一个个add()不好使 List dest = Arrays.asList(new Object[list.size()]); Collections.copy(dest,list); System.out.println(dest); //replaceAll(List list,Object old,Object new) Collections.replaceAll(list,12,22); System.out.println(list); } }
public void test3(){ ArrayList list = new ArrayList(); list.add(1); list.add(2); list.add(3); list.add(4); List list1 = Collections.synchronizedList(list); }
数据结构简述:
第十二章、泛型
一、为什么要有泛型
1.为什么要有泛型
二、泛型的使用
/** * 泛型的使用: * 1.jdk5.0新增的特性 */ public class GenericTest { //在集合中使用泛型之前的情况: @Test public void test(){ ArrayList list = new ArrayList(); //需求:存放学生的成绩 list.add(78); list.add(12); list.add(74); list.add(53); //问题1:类型不安全,没有限制类型 // list.add("Tom"); for(Object score: list){ //需要把每个人的成绩装入studentScore中 //问题2:强转时,可能出现ClassCastException int studentScore = (Integer) score; System.out.println(studentScore); } } //在集合中使用泛型之后的情况:以ArrayList为例 @Test public void test1(){ //为什么不能用int?因为泛型是一个类型,不包含基本数据类型 ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); list.add(32); list.add(24); list.add(23); //编译时,就会进行类型检查,保证数据的安全 // list.add("q");//不通过 for(Integer score : list){ //避免了强转操作 int studentScore = score; System.out.println(studentScore); } Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()){ Integer stuScore = iterator.next(); System.out.println(stuScore); } } //在集合中使用泛型之后的情况:以HashMap为例 @Test public void test3(){ Map<String,Integer> map = new HashMap<String,Integer>(); map.put("age",12); map.put("size",12); map.put("height",12); //泛型的嵌套 Set<Map.Entry<String,Integer>> set = map.entrySet(); Iterator<Map.Entry<String, Integer>> iterator = set.iterator(); while (iterator.hasNext()){ Map.Entry<String, Integer> entry = iterator.next(); System.out.println(entry.getKey() + "->"+ entry.getValue()); } } }
泛型是在jdk5.0时新增的特性
在集合中使用泛型:
-
集合接口或集合类在jdk5.0时都修改为带泛型的结构
-
在实例化集合类时,可以指明具体的泛型类型。
-
指明完之后,在集合类或接口中凡是定义类或接口时,内部结构(比如:方法、构造器、属性)使用到类的泛型的位置,都指定为实例化的泛型类型
比如 : add( E e) ---> 实例化以后:add(Integer e)
-
注意点:泛型的类型必须是类,不能是基本数据类型。需要用到基本数据类型的位置,拿包装类替换。
-
如果实例化时,没有指明泛型的类型。默认类型为java.lang.Object类
jdk7时的新特性,(类型推断 ) 实例化含有泛型的类时,可以省略后面泛型的声明,如:
TreeSet<Employee> set = new TreeSet();
三.如何自定义泛型结构(泛型类、泛型接口;泛型方法)
1.自定义泛型类、泛型接口:
/** * 自定义泛型类:注意,这里是指自定义了一个用了泛型的类,而不是自定义一个类叫T * T只是一个参数,用来代指某个类 */ public class Order <T>{ String orderName; int orderId; //类的内部结构就可以使用类的泛型 T orderT; public Order(){}; public Order(String orderName,int orderId , T orderT){ this.orderId = orderId; this.orderName = orderName; this.orderT = orderT; } //下面的不是泛型方法 public T getOrderT(){ return orderT; } void setOrderT(T t){ this.orderT = t; } @Override public String toString() { return "Order{" + "orderName='" + orderName + '\'' + ", orderId=" + orderId + ", orderT=" + orderT + '}'; } }
public class GenericTest1 { @Test public void test1(){ //如果定义了泛型类,实例化时没有指明类的泛型,则认为此泛型类型为Object类型 //要求:如果大家定义了类是带泛型的,建议在实例化时要指明类的泛型 Order order = new Order(); order.setOrderT(123); order.setOrderT("aa"); //建议:实例化时指明类的类型 Order<String> order1 = new Order<>(); order1.setOrderT("1"); } }
关于泛型类的子类是否 还需用泛型的讨论:
-
情况1:
public class SubOrder extends Order<String>{ }
public void test2(){ //由于子类在继承带泛型的父类时,指明了泛型类型。 //则实例化子类对象时,不再需要指明泛型 SubOrder sub = new SubOrder(); sub.setOrderT("aa"); }
-
情况2:
public class SubOrder1<T> extends Order<T>{ }
//没有指明泛型类型,所以实例化时需要指明(不指明则为Object类) SubOrder1<String> sub1 = new SubOrder1<>(); sub1.setOrderT("aa");
//泛型不能用在静态方法中:原因是静态结构在调用时,还没有造对象,泛型类型未指定 // public static void show(T orderT){ // System.out.println(orderT); // } public void tt(){ // try { // System.out.println("a"); // }catch (T t){ //异常类不能用泛型代替,自定义异常类也不可以带泛型 // // } } public void dd(){ //T someT = new T(); T someT =(T) new Object(); // T[] someMoreT = new T[10]; T[] someMoreT = (T[])new Object[10]; }
2.自定义泛型方法
泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系
换句话说,泛型方法所属的类是不是泛型类都没有关系
//自定义一个泛型方法 public <E> List<E> toListFromArray(E[] e){ ArrayList<E> list = new ArrayList<>(); for(E o : e){ list.add(o); } return list; }
public void test3(){ Order<String> order = new Order<>(); Integer[] arr = new Integer[]{1,2,3}; //泛型方法在调用时,指明泛型参数的类型。 List<Integer> list = order.toListFromArray(arr); System.out.println(list); }
//泛型方法可以是静态的,原因:泛型参数是在调用方法时确定的,并非在实例化类时确定。 public static <E> List<E> toListFromArray(E[] e){ ArrayList<E> list = new ArrayList<>(); for(E o : e){ list.add(o); } return list; }
四、泛型在继承方面的体现
1.类A是类B的父类,G < A > 和G < B > 二者不具备子父类关系,二者是并列关系
@Test public void test1(){ //多态 String s = new String("aa"); Object o = new Object(); o = s; //多态 String[] strings = new String[10]; Object[] objects = new Object[12]; objects = strings; //编译不通过,类型不一致不能赋值 // Date date = new Date(); // date = s; //编译不通过,这两个是并列的类型 List<Object> list = new ArrayList<>(); List<String> list1 = new ArrayList<>(); list = list1; //此时的list1和list2的类型不具有子父类关系 }
2.类A是类B的父类/实现的接口,A< T > 是 B< T > 的父类
public void test2(){ Generic<String> generic = new Generic<>(); SubGeneric<String> subGeneric = new SubGeneric<>(); generic = subGeneric; }
五、通配符的使用
通配符: ?
类A是类B的父类,G< A > 和G< B >是没有关系的,二者共同父类是:G< ? >
@Test public void test3(){ ArrayList<String> list1 = new ArrayList<>(); ArrayList<Integer> list2 = new ArrayList<>(); ArrayList<?> list = null; list = list1; list = list2; print(list1); print(list2); } public void print(List<?> list){ Iterator<?> iterator = list.iterator(); while (iterator.hasNext()){ Object obj = iterator.next(); } }
对于使用通配符的类型:不允许添加数据(除null),允许读入数据
public void test4(){ ArrayList<String> list = new ArrayList<>(); ArrayList<?> list1 = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); list1 = list; //使用通配符的类型的变量 就不能添加数据了 // list1.add("1"); //但是可以添加null list1.add(null); //可以读取数据 System.out.println(list1.get(1)); Iterator<?> iterator = list1.iterator(); while (iterator.hasNext()){ Object next = iterator.next(); System.out.println(next); } }
有限制的通配符:
有限制条件的通配符的使用:
-
?extends A :
G<? extends A>可以作为G< A >和G< B > 的父类,其中B是A的子类
-
?super A :
G<? super A > 可以作为G< A > 和G< B >的父类,其中B是A的父类
/* 有限制条件的通配符的使用 */ @Test public void test5(){ List<? extends Person> list1 = null; List<? super Person> list2 = null; List<Student> list3 = null; List<Person> list4 = null; List<Object > list5 = null; list1 = list3; list1 = list4; // list1 = list5;//报错 // list2 = list3;//报错 list2 = list4; list2 = list5; //读取数据 List<? extends Person> list6 = new ArrayList<Student>(); list6.add(null); Person person = list6.get(0); //list6.get(0)返回的是一个继承于Person类或Person类自身的一个对象。 //所以list6.get(0)的引用只能是大于Person类的。 // Student student = list6.get(0);//报错 List<? super Person> list7 = new ArrayList<Person>(); list7.add(null); // Person person1 = list7.get(0);//报错 Object object = list7.get(0); //写入数据 //list1.add(new Student()); //编译不通过 //原因:List<? extends Person>类型的list只能盛装小于等于Person的 // ,如果比Student还小,那么当然不能放入Student类的对象 //编译通过 list2.add(new Person(12,"2")); list2.add(new Student()); }
?的意思:
-
? 代表可以List<?>是任意List< A >的父类
-
?代表List<?>中可以存储任意类型的元素
对通配符的解释:以List为例
List<?> 是一个类型,可以作为List< A >的父类,其中A是任意类型。
所以可以用多态的知识解决赋值、读取数据、写入数据的情况:
(1)父类的引用指向子类的对象:
List<?> list = new ArrayList < String >( );
(2)读取数据:
Object obj= list.get(0); // 由于list.get()返回的类型为capture of ? ,可能是任何类型,所以只能由Object接收
(3)写入数据
由于创建List<?> 类型的对象时,可能是任意指定泛型的集合,所以在写入数据时,这个数据的类型可能是任何类型,类型未知,我怎么能让它指向一个具体的对象呢?如果该类型比具体的对象的类型小,就违背多态了。所以只能写null
List<? extends Person> 这是一个类型,可以作为List< A >的父类,其中A是Person类或者Person类的子类。
所以可以用多态的知识解决赋值、读取数据、写入数据的情况:
(1)父类的引用指向子类的对象:
List<? extends Person> list1 = new ArrayList < Student>( );
(2)读取数据:
Person obj= list1.get(0); // 由于list.get()返回的类型为capture of ?extends Person ,可能是任何Person类的子类,所以只能由Person类及Person类的父类接收
(3)写入数据
由于创建List<? extends Person> 类型的对象时,可能是任意Person类子类的集合,所以集合的元素的类型可以达到无限小,元素指向的对象必须保证比元素的类型更小,这是无法做到的,所以只能写null;
List<? super Person> 这是一个类型,可以作为List< A >的父类,其中A是Person类或者Person类的父类。
所以可以用多态的知识解决赋值、读取数据、写入数据的情况:
(1)父类的引用指向子类的对象:
List<? super Person> list2 = new ArrayList < Object>( );
(2)读取数据:
Object obj= list2.get(0); // 由于list.get()返回的类型为capture of ?super Person ,可能是任何Person类的父类,所以只能由Object类接收
(3)写入数据
由于创建List<? super Person> 类型的对象时,可能是任意Person类父类的集合,所以集合的元素的类型可以达到无限大,元素指向的对象必须保证比元素的类型更小,因此只要写入的对象类型小于等于Person,就可以写入
第十三章、IO流
一、File类的使用
-
File类的一个对象,代表一个文件或者一个文件目录(俗称:文件夹)
-
File类声明在java.io包下
1.相对路径:相较于某个路径下,指明的路径。
绝对路径:包含盘符在内的文件或文件目录的路径
-
(IDEA中)如果开发使用JUnit中的单元测试方法测试,相对路径即为当前Module下。如果使用main()方法测试,相对路径即为当前的Project下
-
(Eclipse)不管使用单元测试方法还是使用main()测试,相对路径都是当前的Project下。
2.关于路径分隔符:
\ :转义字符 、 windows和DOS默认的路径分隔符
\ \:java中的路径分隔符(为了和转义字符区分)
/ : UNIX 和URL 的路径分隔符
File.separator : 根据操作系统,动态的提供分隔符
3.如何创建File类的实例:调用构造器
-
public File(String pathname)
-
public File(String parentPath, String childPath)
-
public File(File parentFile, String childPath)
//三种构造器 public void test(){ //构造器1: File file1 = new File("hello.txt"); // 相对路径 File file2 = new File("D:"+File.separator+"java\\myCode\\baceExercise\\IdeaCode\\JavaSenior\\day10\\he.txt");//绝对路径 System.out.println(file1); System.out.println(file2); //构造器2: File file3 = new File("D:\\java\\myCode\\baceExercise\\IdeaCode\\JavaSenior","day10"); System.out.println(file3); //构造器3: File file4 = new File(file3,"num.txt"); System.out.println(file4); }
==new 一个File类型对象只是加载在内存中,不是真实文件,如果要在硬盘中创建文件,需要调用createNewFile()/mkdir()/mkdirs().==
4.File类中方法的使用:
File类重写toString() : 返回文件绝对路径
@Test public void test2(){ File file1 = new File("hello.txt"); File file2 = new File("d:\\io","he.txt"); /* getAbsolutePath() getPath() getName() getParent() length() lastModified() */ System.out.println(file1.getAbsoluteFile()); System.out.println(file1.getPath()); System.out.println(file1.getName()); System.out.println(file1.getParent()); System.out.println(file1.length()); System.out.println(file1.lastModified()); System.out.println("***************"); System.out.println(file2.getAbsoluteFile()); System.out.println(file2.getPath()); System.out.println(file2.getName()); System.out.println(file2.getParent()); System.out.println(file2.length()); System.out.println(file2.lastModified()); } @Test public void test3(){ //以下的两个方法只能适用于目录(文件夹) //String[] list(); File file = new File("D:\\java\\myCode\\baceExercise\\IdeaCode\\JavaSenior"); String[] list = file.list(); for(String s : list){ System.out.println(s); } //File[] listFiles(): File[] files = file.listFiles(); for(File file1 : files){ System.out.println(file1); } } @Test public void test4(){ //public boolean renameTo(File dest):把文件重命名为指定的文件路径 //以file1.renameTo(file2)为例: //要想保证返回true,需要file1在硬盘中是存在的,且file2不能在硬盘中存在 File file1 = new File("hello.txt"); File file2 = new File("d:\\io\\hi.txt"); boolean b = file1.renameTo(file2); //调用的结果是本来处于原路径下的文件转到新路径下,且文件改名了 //根本来说就是换了个路径 System.out.println(b); }
public void test5(){ File file1 = new File("hello.txt"); File file2 = new File("d:\\io\\hi.txt"); System.out.println(file1.isDirectory());//false System.out.println(file1.isFile());//true System.out.println(file1.exists());//true System.out.println(file1.canRead());//true System.out.println(file1.canWrite());//true System.out.println(file1.isHidden());//false File file3 = new File("d:\\io"); System.out.println(file3.isDirectory());//true System.out.println(file3.isFile());//false System.out.println(file3.exists());//true System.out.println(file3.canRead());//true System.out.println(file3.canWrite());//true System.out.println(file3.isHidden());//false //注:这里的hello.txt 和 d:\\io都是硬盘上真实存在的(调用exists()为true), //如果不是真实存在,那么上述所有方法都返回默认值 }
public void test6() throws IOException { //创建文件 //createNewFile():创建新文件(创建进硬盘中的) File file1 = new File("hi.txt"); if (! file1.exists()){ file1.createNewFile(); System.out.println("创建成功"); }else { //文件存在 file1.delete(); System.out.println("删除成功" ); } //创建文件目录 //mkdir() File file2 = new File("d:\\io\\io1"); boolean mkdir = file2.mkdir(); if (mkdir ==true){ System.out.println("创建成功"); }else { System.out.println("创建失败"); } //mkdirs() File file3 = new File("d:\\io\\io2\\inter"); boolean mkdirs = file3.mkdirs(); if (mkdirs == true){ System.out.println("创建成功"); }else { System.out.println("创建失败"); } }
File类不是真实的文件,调用创建的方法后才能在硬盘中创建真实文件
createNewFile() / mkdir() / mkdirs() 三个方法创建文件时,若文件已存在,都不创建,返回false。
createNewFile():若创建的文件的上一级目录不存在,抛异常IOException
mkdir():若创建的目录的上一级目录不存在,则不创建,返回false
mkdirs():若创建的目录的上一级目录不存在,则一并创建,返回true
需要关注点:
5.File类中涉及到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法,并未涉及到写入或读取文件内容的操作。如果需要读取或写入文件内容,必须使用IO流来完成。
6.后续File类的对象常会作为参数传递到流的构造器中,指明读取或写入的”终点“。
关于文件的算法题:(递归操作)
//打印file目录下的所有文件(不包括文件目录) //思路:先取出目录下所有文件/文件目录 ,遍历,若是文件,直接打印,若是文件目录,再次调用该方法 public void printAllFiles(File file){ File[] list = file.listFiles(); for (int i = 0; i < list.length; i++) { if ( ! list[i].isDirectory()){ System.out.println(list[i]); }else { printAllFiles(list[i]); } } } //删除file文件或文件目录(注:删除文件目录的前提是目录下没有其他文件) //思路:如果file是文件,直接删除,如果是文件目录,取出目录下所有文件/文件目录,遍历,再次调用该方法 public void deleteAll(File file){ if (file.isDirectory()){ File[] list = file.listFiles(); for(File f : list){ deleteAll(f); } } file.delete(); }
二、IO流原理及流的分类
1.IO原理
输入输出是相对的概念,我们要站在程序(内存)的角度,数据由硬盘/网络传输到内存中,是输入,数据由内存传输到硬盘/网络 为输出
2.流的分类:
-
按操作数据单位不同:(byte)字节流(8 bit)、(char)字符流(16 bit)
-
按数据流的流向不同分为:输入流、输出流
-
按流的角色的不同分为:节点流、处理流
字符流是一个个的char,所以适合存储文本文件(.txt) , 字节流则适合存储视频、图片
什么是节点流和处理流:
节点流是直接操作数据的流
处理流是包在节点流外面的,用于添加功能的流
IO流体系:
####
一、流的分类: * 字符流、字节流;输入流、输出流;节点流、处理流 * 二、流的体系结构: * 抽象基类: 节点流(或文件流) 缓冲流(处理流的一种) * InputStream FileInputStream BufferedInputStream * OutputStream FileOutputStream BufferedOutputStream * Reader FileReader BufferedReader * Writer FileWriter BufferedWriter
三、文件流(节点流)的使用
1.FileReader读入数据的基本操作
@Test public void test() throws IOException { /* 将day10下的hello.txt文件内容读入程序中,并输出到控制台 */ //1.实例化File类的对象,指明要操作的文件 File file = new File("hello.txt"); //路径相较于当前的module //2.提供具体的流 FileReader fr = new FileReader(file); //3.数据的读入 //read():返回读入的一个字符。如果达到文件末尾,返回-1 int data ; while ((data = fr.read()) != -1){ System.out.print((char)data); } //4.流的关闭操作 fr.close(); }
上述代码有缺陷:应改为:
@Test public void test() { /* 将day10下的hello.txt文件内容读入程序中,并输出到控制台 */ FileReader fr = null; //这个必须声明在try外部,因为finally中也要调用 try { //1.实例化File类的对象,指明要操作的文件 File file = new File("hello.txt"); //路径相较于当前的module //2.提供具体的流 fr = new FileReader(file); //3.数据的读入 //read():返回读入的一个字符。如果达到文件末尾,返回-1 int data ; while ((data = fr.read()) != -1){ System.out.print((char)data); } } catch (IOException e) { e.printStackTrace(); } finally { //4.流的关闭操作 try { if (fr != null) //防止出现空指针异常 fr.close(); } catch (IOException e) { e.printStackTrace(); } } }
说明点:
-
read()的理解:返回读入的一个字符(int 类型)。如果达到文件末尾,返回-1
-
异常的处理:为了保证流资源一定可以执行关闭操作,需要使用try-catch-finally
-
读入的文件一定要存在,否则会报FileNotFoundException
-
因为FileReader对象是定义在try结构外部,初始化为null,所以finally中调用close()方法时有必要判断是否是空,防止空指针异常
如果流的创建是成功的,但是read()这里出现异常,那么close()方法无法关闭,就会造成资源泄露,所以我们改写成try-catch-finally
对read()操作升级:使用read()的重载方法 read(char[] cbuf):将cbuf 数组长度的个数的字符放入cbuf中,并 返回每次读入cbuf数组中的字符的个数,注:==如果某次读入的数量达不到数组的长度,那么返回的值是小于数组长度的== 如果达到文件末尾,返回-1
public void test2() { /* 对read()操作升级:使用read()的重载方法 read(char[] cbuf):返回每次读入cbuf数组中的字符的个数, 如果达到文件末尾,返回-1 */ FileReader fr = null; try { //1.File 类的实例化 File file = new File("hello.txt"); //2.流的实例化(这里是字符输入流) fr = new FileReader(file); //3.读入的操作 //read(char[] cbuf):返回每次读入cbuf数组中的字符的个数, //如果达到文件末尾,返回-1 char[] cbuf = new char[5]; int len; while ((len = fr.read(cbuf)) != -1){ //以下是IO流中的难点 //读取方式一: //错误的写法 // for (int i = 0; i < cbuf.length; i++) { // System.out.print(cbuf[i]); // } //正确的写法 // for (int i = 0; i < len; i++) { // System.out.print(cbuf[i]); // } //读取的方式二: //错误的写法 // String s = new String(cbuf); // System.out.print(s); //正确的写法 // String s = new String(cbuf,0,len); // System.out.print(s); } } catch (IOException e) { e.printStackTrace(); } finally { //4.资源的关闭 try { if(fr != null) fr.close(); } catch (IOException e) { e.printStackTrace(); } } }
自己动手写一遍
public void test3() { FileReader fr = null; try { File file = new File("hello.txt"); fr = new FileReader(file); char[] cbuf = new char[5]; StringBuffer result = new StringBuffer() ; int len; while ((len = fr.read(cbuf)) != -1){ String s = new String(cbuf , 0 , len); result.append(s); } System.out.println(result); } catch (IOException e) { e.printStackTrace(); } finally { try { if (fr!= null) fr.close(); } catch (IOException e) { e.printStackTrace(); } } }
2.从内存中写出数据到硬盘的文件里。
public void test4() { FileWriter fw = null; try { //1.提供File类的对象,指明写出到的文件 File file = new File("hello1.txt"); //2.提供FileWriter的对象,用于数据的写出 fw = new FileWriter(file); //3.写出的操作 fw.write("I have a dream\n".toCharArray()); fw.write("you need to have a dream,too"); //4.流资源的关闭 } catch (IOException e) { e.printStackTrace(); } finally { try { if (fw!= null) fw.close(); } catch (IOException e) { e.printStackTrace(); } } }
说明:
-
输出操作,对应的File可以不存在的。并不会报异常
-
File对应的硬盘中的文件:
-
如果不存在,在输出的过程中,会自动创建此文件
-
如果存在:
-
如果流使用的构造器是FileWriter(file,false) / FileWriter(file): 对原有文件的覆盖,即清空原文件所有内容,再加入新的内容
-
如果流使用的构造器是FileWriter(file,true): 不会对原有文件覆盖,而是在原有文件基础上追加内容
-
-
关于FileWriter的构造器:
-
只有文件一个参数 : FileWriter fw = new FileWriter(file): 这之后调用write()是覆盖写
-
有两个参数:FileWriter fw = new FileWriter(file , append: true/false): append的值为true表示追加写,为false表示覆盖下写
read()方法的重载构造器:
-
read(): 读入一个字符,以int类型出现
-
read(char[] cbuf): 每次读cbuf.length个字符进cbuf数组中,并返回读入字符的个数
-
//少用:read(char[] cbuf , int off,int len): 从off位置开始,将len个字符读入数组中。
write()方法的重载构造器:
-
write(String s) : 将s写出
-
write(char[] charArray): 将charArray中的字符逐个写出
-
write(String s, int off ,int len): 从off开始,将len个字符写出文件
-
write(char[] charArray, int off,int len): 从off开始,将len个字符写出文件
3.先读取A文件内容,再写到B文件中去.(可以实现文件的复制)
public void test5() { FileReader fr = null; FileWriter fw = null; try { //1.创建File类对象,指明读入和写出的文件 File srcFile = new File("hello.txt"); File destFile = new File("hello1.txt"); //2.创建输入流和输出流的对象 fr = new FileReader(srcFile); fw = new FileWriter(destFile,true); //3.数据的读入和写出操作 //读入read字符串中 char[] cbuf = new char[5]; int len;//记录每次读入到cbuf数组的字符串的个数 while((len=fr.read(cbuf)) != -1){ //每次写出len个字符 fw.write(cbuf, 0 ,len); } } catch (IOException e) { e.printStackTrace(); } finally { //4.关闭流资源 try { if (fr != null){ fr.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (fw != null){ fw.close(); } } catch (IOException e) { e.printStackTrace(); } } }
关于try-catch-finally的小复习:
try-catch是处理异常的一种方式,如果try中有异常,catch处理了,try-catch后面的内容仍可执行。
finally是一定会执行,不管是有异常还是有return
使用字符流处理.jpg等字符文件时,会出现问题。所以要用字节流才行
4.测试FileInputStream和FileOutputStream处理文本文件:可能出现乱码
@Test public void test7() { FileInputStream fis = null; try { File file = new File("hello.txt"); fis = new FileInputStream(file); byte[] bytes = new byte[5]; int len; while ((len = fis.read(bytes)) != -1){ String s = new String(bytes,0,len); System.out.print(s); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fis !=null) fis.close(); } catch (IOException e) { e.printStackTrace(); } } }
如果hello.txt中存放的全是英文,可以正常输出,因为英文字符在-128~127内,可以转化为一个个的字节;而如果有中文,一个中文占三个字节,在放入bytes数组中可能会被拆开,从而出现乱码,这种情况可以通过增大数组长度改善,但是治标不治本。
但是,如果我不需要在控制台输出文本,而是只想复制一份,也可以用字节流操作,因为底层都是字节,之所以看的时候出现乱码是因为将字节转为字符 在数组中的操作有问题。
==总结:==
-
对于文本文件(.txt, .java, .c, .cpp...),使用字符流处理
-
对于非文本文件(.jpg, .mp3, .mp4 ,.avi , .doc, .ppt ....),使用字节流处理
4.测试FileInputStream和FileOutputStream处理非文本文件(可实现非文本文件的复制)
public void test6() { FileInputStream fis = null; FileOutputStream fos = null; try { File srcFile = new File("IMG20211204133644.jpg"); File destFile = new File("宝贝.jpg"); fis = new FileInputStream(srcFile); fos = new FileOutputStream(destFile); byte[] cbuf = new byte[10]; int len; while ((len = fis.read(cbuf)) != -1){ fos.write(cbuf,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } }
四、缓冲流(处理流的一种)的使用
1.缓冲流:
BufferedInputStream
BufferedOutStream
BufferedReader
BufferedWriter
2.作用:提高流的读取、写入的速度
提高读写速度的原因:内部提供了一个缓存区(缓冲阈值:8192)
==BufferedOutputStream和BufferedWriter中有个flush()方法作用是刷新缓冲区,即使没到8192,也全部输出==
3.用缓冲流实现非文本文件的复制
缓冲流需要节点流作为参数创建缓冲流对象。
所以要先造节点流,再造缓冲流
public void test1() { /* 实现非文本文件的复制 */ FileInputStream fis = null; FileOutputStream fos = null; BufferedInputStream bis = null; BufferedOutputStream bos = null; try { //1.造文件 File srcFile = new File("宝贝.jpg"); File destFile = new File("宝贝1.jpg"); //2.造流 //2.1 造节点流 fis = new FileInputStream(srcFile); fos = new FileOutputStream(destFile); //2.2 造缓冲流 bis = new BufferedInputStream(fis); bos = new BufferedOutputStream(fos); //3.复制的细节:读取、写入 byte[] buffer = new byte[100]; int len; while ((len = bis.read(buffer)) != -1){ bos.write(buffer,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { //4.资源关闭 //要求:先关闭外层的流,再关闭内层的流 try { if(bos!=null) bos.close(); } catch (IOException e) { e.printStackTrace(); } try { if (bis!=null) bis.close(); } catch (IOException e) { e.printStackTrace(); } //说明:在关闭外层流的同时,内层流也会自动的进行关闭。 //所以关于内层流的关闭,我们可以省略 // fos.close(); // fis.close(); } }
4.缓冲流与节点流读写速度对比
//指定路径下文件的复制的方法:使用节点流 public void copyWithFile(String src,String dest) { FileInputStream fis = null; FileOutputStream fos = null; try { File srcFile = new File(src); File destFile = new File(dest); fis = new FileInputStream(srcFile); fos = new FileOutputStream(destFile); byte[] cbuf = new byte[100]; int len; while ((len = fis.read(cbuf)) != -1) { fos.write(cbuf, 0, len); } } catch (IOException e) { e.printStackTrace(); } finally { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } //指定路径下文件的复制的方法:使用缓冲流 public void copyWithBuffered(String srcPath,String destPath) { /* 实现非文本文件的复制 */ FileInputStream fis = null; FileOutputStream fos = null; BufferedInputStream bis = null; BufferedOutputStream bos = null; try { //1.造文件 File srcFile = new File(srcPath); File destFile = new File(destPath); //2.造流 //2.1 造节点流 fis = new FileInputStream(srcFile); fos = new FileOutputStream(destFile); //2.2 造缓冲流 bis = new BufferedInputStream(fis); bos = new BufferedOutputStream(fos); //3.复制的细节:读取、写入 byte[] buffer = new byte[100]; int len; while ((len = bis.read(buffer)) != -1){ bos.write(buffer,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { //4.资源关闭 //要求:先关闭外层的流,再关闭内层的流 try { if(bos!=null) bos.close(); } catch (IOException e) { e.printStackTrace(); } try { if (bis!=null) bis.close(); } catch (IOException e) { e.printStackTrace(); } //说明:在关闭外层流的同时,内层流也会自动的进行关闭。 //所以关于内层流的关闭,我们可以省略 // fos.close(); // fis.close(); } } @Test public void test3(){ //测试用与不用缓冲流的效率差距 long start1 = System.currentTimeMillis(); copyWithFile("宝贝.jpg","宝贝4.jpg"); long end1 = System.currentTimeMillis(); System.out.println("节点流复制花费时间:"+(end1 - start1)); long start2 = System.currentTimeMillis(); copyWithBuffered("宝贝.jpg","宝贝5.jpg"); long end2 = System.currentTimeMillis(); System.out.println("缓冲流复制花费时间:"+(end2 - start2)); }
输出结果:
节点流复制花费时间:450 缓冲流复制花费时间:14
5.处理流,就是”套接“在已有的流(不仅是节点流)的基础上。
6.使用缓冲流实现文本文件的复制
这里提到了BufferedReader里的另外一个读取方法readLine(),读取一行数据,返回字符串,且无参数。与read()有很大区别,read()是把数组作为参数,读取的数据放入数组中。
public void test5(){ /* 使用BufferedReader和BufferedWriter实现文本文件的复制 */ BufferedReader bf = null; BufferedWriter bw = null; try { bf = new BufferedReader(new FileReader(new File("hello1.txt"))); bw = new BufferedWriter(new FileWriter(new File("hello2.txt"))); //BufferedReader提供了另外一种读取方式: //原本的方式1: // char[] cbuf = new char[10]; // int len; // while ((len = bf.read(cbuf)) != -1){ // bw.write(cbuf,0,len); // } //方式二: String s; while ((s = bf.readLine()) != null){ bw.write(s);//readline()不能读到换行符,所以需要手动添加 //方式一: bw.write("\n"); //方式二: bw.newLine(); } } catch (IOException e) { e.printStackTrace(); } finally { if (bf != null) { try { bf.close(); } catch (IOException e) { e.printStackTrace(); } } if (bw != null) { try { bw.close(); } catch (IOException e) { e.printStackTrace(); } } } }
小结一下:
除此之外:BufferedOutputStream 和BufferedWriter 都有flush()方法刷新缓冲区
五、转换流(处理流之二)的使用
1.转换流:==属于字符流==
InputStreamReader : 将一个字节的输入流转换为字符的输入流
OutputStreamWriter : 将一个字符的输出流转换为字节的输出流
InputStreamReader 继承于Reader类
OutputStreamWriter 继承于Writer类
2.作用:提供字节流与字符流之间的转换
3.解码:字节、字节数组 --->字符数组、字符串
编码:字符数组、字符串---->字节、字节数组
4.字符集:在创建InputStreamReader对象时可以指定字符集,要求字符集需要和保存文件时使用的一样,不然会出现乱码.
5.InputStreamReader的使用:实现字节输入流到字符输入流的转换
public void test1() { InputStreamReader isr1 = null; try { FileInputStream fis = new FileInputStream("hello.txt"); // InputStreamReader isr = new InputStreamReader(fis);//使用系统默认的字符集 isr1 = new InputStreamReader(fis,"utf-8"); //参数二指明了字符集,具体使用哪个字符集,取决于"hello.txt"保存时使用的是哪个 char[] cbuf = new char[20]; int len; while ((len = isr1.read(cbuf))!= -1){ String str = new String(cbuf,0,len); System.out.println(str); } } catch (IOException e) { e.printStackTrace(); } finally { if (isr1!=null){ try { isr1.close(); } catch (IOException e) { e.printStackTrace(); } } } }
6.综合使用InputStreamReader 和 OutputStreamWriter
(将原文件换一个编码集存储)
public void test4(){ //综合使用InputStreamReader 和 OutputStreamWriter InputStreamReader isr = null; OutputStreamWriter osw = null; try { //1.造文件,造流 File file1 = new File("hello1.txt"); File file2 = new File("hello3.txt"); FileInputStream fis = new FileInputStream(file1); FileOutputStream fos = new FileOutputStream(file2); isr = new InputStreamReader(fis,"utf-8"); osw = new OutputStreamWriter(fos,"gbk"); //2.读写操作 char[] cbuf = new char[20]; int len; while ((len = isr.read(cbuf))!= -1){ osw.write(cbuf,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { if (isr != null){ try { isr.close(); } catch (IOException e) { e.printStackTrace(); } } if (osw !=null){ try { osw.close(); } catch (IOException e) { e.printStackTrace(); } } } }
7.多种字符编码集的说明
六、其他流的使用
1.标准的输入流、输出流:
public class OtherStreamTest { /* 1.标准的输入、输出流: 1.1 System.in:标准的输入流,默认从键盘输入 System.out:标准的输出流,默认从控制台输出 1.2 System类的setIn(InputStream is)/setOut(PrintStream ps)方式重新指定输入和输出流 1.3 从键盘输入字符串,要求将读取到的整行字符串转成大写输出。然后继续进行输入操作 直至当输入”e“或者”exit“时,退出程序。 方法一:使用Scanner实现,调用next(System.in)返回一个字符串 方法二:使用System.in实现: System.in是InputStream类型,转换流是Reader/Writer类型,BufferedReader是Reader类型 System.in--->转换流--->BufferedReader 调用BufferedReader的readLine()方法 */ public static void main(String[] args) { BufferedReader br = null; try { InputStreamReader isr = new InputStreamReader(System.in); br = new BufferedReader(isr); while (true){ System.out.println("请输入字符串"); String data = br.readLine(); //捕获从键盘输入的数据 if ("e".equalsIgnoreCase(data)||"exit".equalsIgnoreCase(data)){ System.out.println("程序结束"); break; } String upperCase = data.toUpperCase(); System.out.println(upperCase); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (br!=null) br.close(); } catch (IOException e) { e.printStackTrace(); } } } }
手动实现Scanner类
public class MyScanner { public static String get() throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader()); String s = br.readLine(); // br.close(); return s; } public static int getInt() throws IOException { int i = Integer.parseInt(get()); return i ; } public static void main(String[] args) throws IOException { // String s = get(); // System.out.println(s); int i = getInt(); System.out.println(i+1); } }
2.打印流
练习:
将本该在控制台输出的东西保存到文件中
@Test public void test(){ PrintStream newPrint = null; try { FileOutputStream fos = new FileOutputStream("hello6.txt");//修改输出的终点为"hello6.txt" newPrint = new PrintStream(fos); System.setOut(newPrint); for (int i = 0; i < 255; i++) { System.out.print((char) i); } } catch (FileNotFoundException e) { e.printStackTrace(); } finally { newPrint.close(); } }
3.数据流:
DataInputStream
DataOutputStream
(1)作用:用于读取或写出==基本数据类型的变量或字符串==
练习:将内存中的字符串、基本数据类型写入文件中,再读取出来
注:用数据流写入的文件不是用来直接打开看的,而是通过数据流再依次读取
而且用数据流再读取不同类型的数据的顺序要与当初写入文件时保存的数据顺序一致
@Test public void test() { /* 用数据流写入基本数据类型或字符串 */ DataOutputStream dos = null; try { dos = new DataOutputStream(new FileOutputStream("data.txt")); dos.writeUTF("程刚"); dos.flush();//刷新操作,将内存中的数据写入文件 dos.writeInt(21); dos.flush(); dos.writeBoolean(true); dos.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if (dos!=null){ try { dos.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Test public void test1(){ /* 用数据流读取基本数据类型或字符串 */ DataInputStream dis = null; try { dis = new DataInputStream(new FileInputStream("data.txt")); System.out.println(dis.readUTF()); System.out.println(dis.readInt()); System.out.println(dis.readBoolean()); } catch (IOException e) { e.printStackTrace(); } finally { if(dis !=null){ try { dis.close(); } catch (IOException e) { e.printStackTrace(); } } } }
七、对象流的使用
1.
ObjectInputStream
ObjectOutputStream
2.对象流的作用:存储和读取==基本数据类型的数据 或 对象==
3.对象的序列化机制:(重要)
要将对象放入文件中持久化保存,该对象必须是可序列化的
4.ObjectInputStream和ObjectOutputStream的使用:序列化和反序列化
/* 序列化过程:将内存中的java对象保存到磁盘中或通过网络传输出去 使用ObjectOutputStream实现 */ @Test public void test(){ ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(new FileOutputStream("object.dat")); oos.writeObject(new String("hello")); oos.flush();//刷新操作 oos.writeObject(LocalDateTime.now()); oos.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if(oos!=null){ try { oos.close(); } catch (IOException e) { e.printStackTrace(); } } } } /* 反序列化:将磁盘文件中的对象还原为内存中的一个java对象 使用ObjectInputStream实现 */ @Test public void test1(){ ObjectInputStream ois = null; try { ois = new ObjectInputStream(new FileInputStream("object.dat")); System.out.println(ois.readObject()); System.out.println(ois.readObject()); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { if (ois !=null){ try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } } }
5.自定义可序列化的类:
该类需要满足以下条件。方可序列化
-
需要实现接口:Serializable
-
当前类提供一个全局常量:serialVersionUID
-
除了当前Person类需要实现Serializable接口之外,还需要保证其内部所有属性也必须是可序列化的,否则抛异常NotSerializableException。(默认情况下,基本数据类型可序列化)
//自定义可序列化的类 //步骤:实现Serializable接口,提供全局常量serialVersionUID,保证所有属性都是可序列化的 class Person implements Serializable{ private String name; private int age; private static final long serialVersionUID = 244213123124L; public Person(String name, int age) { this.name = name; this.age = age; } public Person() { } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
6.自定义的类实现序列化以后就可以通过对象流写入文件保存,再从文件中反序列化为真实对象。
oos.writeObject(new Person("Tom",21)); oos.flush();
Person p = ois.readObject();
7.对于SerialVersionUID的理解:(序列版本号)
一个可序列化的如果没有添加SerialVersionUID,在序列化(将对象的数据存入文件)时,会自动生成一个UID,如果此后对该类进行修改,自动生成的UID会随之变化,那么反序列化时就找不到原来的对象了。
8.ObjectOutputStream 和 ObjectInputStream 不能序列化static和transient修饰的成员变量。
static是类所有的,不是对象拥有,所以不可序列化
transient是专门用来指示该对象不可序列化的。
前面提到自定义类可序列化的要求包括所有属性都要可序列化,否则抛异常NotSerializableException。如果某些属性被static和transient修饰了,该类仍可序列化,只是被修饰的属性不可序列化,如果有一些通过网络传输的属性不想被序列化(隐私,密码等),就可以修饰,这样在传输类的对象时,不会传输该属性。
八、随机存取文件流 (属于字节流)
1.RandomAccessFile直接继承于java.lang.Object类(而不是四个抽象基类中的一个),实现了DataInput和DataOutput接口。
2.RandomAccessFile既可以作为一个输入流,又可以作为一个输出流
3.RandomAccessFile直接以File类对象或文件路径作为构造器参数,实例化对象,不需要借助其他流
4.RandomAccessFile构造器的第二个参数用来指示操作,只读,读写。。
RandomAccessFile的应用(复制文件),既可读,也可写
public void test1() { RandomAccessFile raf1 = null; RandomAccessFile raf2 = null; try { raf1 = new RandomAccessFile(new File("宝贝.jpg"),"r"); raf2 = new RandomAccessFile("宝贝1.jpg", "rw"); byte[] buffer = new byte[1024]; int len; while ((len = raf1.read(buffer)) != -1){ raf2.write(buffer,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { if (raf1!=null){ try { raf1.close(); } catch (IOException e) { e.printStackTrace(); } } if (raf2!= null){ try { raf2.close(); } catch (IOException e) { e.printStackTrace(); } } } }
5.==如果RandomAccessFile作为输出流时,写出到的文件如果不存在,则在执行过程中自动创建;==
==如果写出到的文件存在,则会对原有文件内容进行覆盖。(默认情况下,从头覆盖)== 是一个一个字符/字节覆盖,而不是覆盖整个文件内容,这与之前的输出流有很大区别
6.可以通过相关的操作,实现RandomAccessFile"插入"数据的效果(这是其他流做不到的)
/* 使用RandomAccessFile实现数据的插入效果 */ @Test public void test3() throws IOException { RandomAccessFile raf = new RandomAccessFile("hello.txt","rw"); raf.seek(3); //保存指针3后面的所有数据到StringBuilder中 StringBuilder builder = new StringBuilder((int) new File("hello.txt").length()); byte[] buffer = new byte[20]; int len; while ((len = raf.read(buffer)) != -1){ builder.append(new String(buffer,0,len)); } //调回指针,写入xyz raf.seek(3); raf.write("xyz".getBytes()); //将StirngBuilder中的数据再写入文件中 raf.write(builder.toString().getBytes()); raf.close(); }
如果要操作两个文件,需要两个流,如果对一个文件在一个程序中进行读写,可以只创建一个RandomAccessFile类,分别调用read() 和write()
7.补充:ByteArrayOutputStream流的使用,相当于替代了上面程序的StringBuilder的功能
public void test4() throws IOException { RandomAccessFile raf = new RandomAccessFile("hello.txt", "rw"); raf.seek(5);//调指针位置 ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[20]; int len; while ((len = raf.read(buffer))!= -1){ baos.write(buffer,0,len); } raf.seek(5); raf.write("xyz".getBytes()); raf.write(baos.toString().getBytes()); raf.close(); baos.close(); }
8.'插入''的意义
9.开发时都是导入jar包,复制文件等等操作,具体看尚硅谷康师傅p619
九、NIO.2中Path、Paths、Files类的使用
第十四章、网络编程
一、网络编程概述
如何解决准确找到对方主机以及应用,并可靠高效的进行数据传输呢?
二、网络通信要素概述
1.网络编程中的两个要素:
对应问题一:IP和端口号
对应问题二:提供网络通信协议:TCP/IP参考模型(应用层、传输层、网络层、物理+数据链路层)
三、网络通信要素1:IP和端口号
(一)、IP
1.IP:唯一的标识Internet上的计算机(通信实体)
2.在java中使用InetAddress类表示IP地址
3.IP分类:IPV4和IPV6 ; 万维网和局域网
4.域名:www.baidu.com www.mi.com www.sina.com
我们也可以通过域名访问IP地址(为什么要有域名?因为IP地址不易记忆)
DNS:域名解析服务器
通过域名访问网络服务器的流程
5.本地回路地址:127.0.0.1 对应着:localhost (本地服务器的ip地址)
6.如何实例化InetAddress:
InetAddress类的两个静态方法:
-
getByName(String host):host 既可以填IP地址,也可以填域名
-
getLocalHost():返回本地IP
7.InetAddress对象的两个常用方法:
-
getHostName(): 获取主机的域名
-
getHostAddress(): 获取主机的地址
public static void main(String[] args) { try { //实例化的两个方法: InetAddress inet1 = InetAddress.getByName("192.168.10.14"); System.out.println(inet1); InetAddress inet2 = InetAddress.getByName("www.atguigu.com"); System.out.println(inet2); InetAddress inet3 = InetAddress.getByName("127.0.0.1"); System.out.println(inet3); //获取本地地址的方法 InetAddress inet4 = InetAddress.getLocalHost(); System.out.println(inet4); //生成的ip对象的两个方法: System.out.println(inet2.getHostAddress()); System.out.println(inet2.getHostName()); } catch (UnknownHostException e) { e.printStackTrace(); } }
输出结果:
/192.168.10.14 www.atguigu.com/222.192.186.41 /127.0.0.1 LAPTOP-L94DBFBF/10.199.44.121 222.192.186.41 www.atguigu.com
(二)、端口号
1.端口号:标识正在计算机上运行的进程
2.要求:不同的进程有不同的端口号
3.端口被规定为一个16位的整数:0~65535
4.端口号与IP地址的组合得出一个网络套接字:Socket
四、网络通信要素二:网络协议
五、TCP网络编程
1.实现TCP的网络编程
例子1:客户端发送信息给服务端,服务端将数据显示在控制台上
@Test public void client(){ Socket socket = null; OutputStream os = null; try { //1.创建Socket对象,指明服务器端的ip和端口号 InetAddress inet = InetAddress.getByName("127.0.0.1"); socket = new Socket(inet, 8848); //2.获取一个输出流,用于输出数据 os = socket.getOutputStream(); //3.写出数据的操作 os.write("i am a pig!".getBytes()); } catch (IOException e) { e.printStackTrace(); } finally { //4.资源的关闭 if(os!=null){ try { os.close(); } catch (IOException e) { e.printStackTrace(); } } if (socket!=null){ try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Test public void server() { ServerSocket ss = null; Socket socket = null; InputStream is = null; ByteArrayOutputStream baos = null; try { //1.创建服务器端的ServerSocket,指明自己的端口号 ss = new ServerSocket(8848); //2.调用accept()表示接收来自客户端的socket socket = ss.accept(); //3.获取输入流 is = socket.getInputStream(); //使用转换流不兼容非文本文件 //所以使用ByteArrayOutputStream //4.读取输入流中的数据 baos = new ByteArrayOutputStream(); byte[] buffer = new byte[10]; int len; while ((len = is.read(buffer)) != -1){ baos.write(buffer,0,len); } System.out.println(baos.toString()); System.out.println("收到了来自:"+socket.getInetAddress().getHostName()+"的礼物"); } catch (IOException e) { e.printStackTrace(); } finally { if(baos != null){ try { baos.close(); } catch (IOException e) { e.printStackTrace(); } } if (is !=null){ try { is.close(); } catch (IOException e) { e.printStackTrace(); } } if (socket!=null){ try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } if (ss!=null){ try { ss.close(); } catch (IOException e) { e.printStackTrace(); } } } }
例题2:例题2:客户端发送文件给服务端,服务端将文件保存在本地
我对本题的理解:
//例题2:客户端发送文件给服务端,服务端将文件保存在本地 public class TCPTest2 { @Test public void client(){ Socket socket = null; OutputStream os = null; BufferedInputStream bis = null; try { //这里的ip是指明的服务器的ip InetAddress inet = InetAddress.getByName("127.0.0.1"); socket = new Socket(inet, 440); // os = socket.getOutputStream(); File file = new File("宝贝.jpg"); FileInputStream fis = new FileInputStream(file); bis = new BufferedInputStream(fis); byte[] buffer = new byte[10]; int len; while ((len = bis.read(buffer))!= -1){ os.write(buffer,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { if(bis!=null){ try { bis.close(); } catch (IOException e) { e.printStackTrace(); } } if (os!=null){ try { os.close(); } catch (IOException e) { e.printStackTrace(); } } if (socket!=null){ try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Test public void server(){ ServerSocket ss = null; Socket socket = null; InputStream is = null; BufferedOutputStream bos = null; try { ss = new ServerSocket(440); socket = ss.accept(); is = socket.getInputStream(); File file = new File("宝贝2.jpg"); FileOutputStream fos = new FileOutputStream(file,false); bos = new BufferedOutputStream(fos); byte[] buffer = new byte[10]; int len; while ((len = is.read(buffer))!= -1){ bos.write(buffer,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { if(bos!=null){ try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } if (is!=null){ try { is.close(); } catch (IOException e) { e.printStackTrace(); } } if(socket!=null){ try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } if(ss!=null){ try { ss.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
例题3:从客户端发送文件给服务端,服务端保存到本地,并返回"发送成功"给客户端
public class TCPTest3{ @Test public void client(){ Socket socket = null; OutputStream os = null; BufferedInputStream bis = null; InputStream is = null; ByteArrayOutputStream baos = null; try { //这里的ip是指明的服务器的ip InetAddress inet = InetAddress.getByName("127.0.0.1"); //1. socket = new Socket(inet, 440); //2. os = socket.getOutputStream(); //3. File file = new File("宝贝.jpg"); FileInputStream fis = new FileInputStream(file); //4. bis = new BufferedInputStream(fis); byte[] buffer = new byte[10]; int len; while ((len = bis.read(buffer))!= -1){ os.write(buffer,0,len); } //关闭数据的输出,表示客户端不再输出数据了,这样服务器端的read()方法才能结束 socket.shutdownOutput(); //5.接收来自服务器端的反馈信息 is = socket.getInputStream(); baos = new ByteArrayOutputStream(); byte[] buffer1 = new byte[10]; int len1; while ((len1 = is.read(buffer1))!= -1){ baos.write(buffer1,0,len1); } System.out.println(baos.toString()); } catch (IOException e) { e.printStackTrace(); } finally { //6. if(bis!=null){ try { bis.close(); } catch (IOException e) { e.printStackTrace(); } } if (os!=null){ try { os.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 (is!=null){ try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Test public void server(){ ServerSocket ss = null; Socket socket = null; InputStream is = null; BufferedOutputStream bos = null; OutputStream os = null; try { //1. ss = new ServerSocket(440); //2. socket = ss.accept(); //3. is = socket.getInputStream(); //4. File file = new File("宝贝4.jpg"); FileOutputStream fos = new FileOutputStream(file,false); bos = new BufferedOutputStream(fos); //5. byte[] buffer = new byte[10]; int len; while ((len = is.read(buffer))!= -1){ bos.write(buffer,0,len); } //read()方法是一个阻塞方法,我无法知道客户端是否已经停止输出了,会不会再加入数据? System.out.println("图片下载成功"); //6.服务器端给予客户端反应 os = socket.getOutputStream(); os.write("你好,美女,你很好看".getBytes()); } catch (IOException e) { e.printStackTrace(); } finally { //7. if(bos!=null){ try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } if (is!=null){ try { is.close(); } catch (IOException e) { e.printStackTrace(); } } if(socket!=null){ try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } if(ss!=null){ try { ss.close(); } catch (IOException e) { e.printStackTrace(); } } if (os!=null){ try { os.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
由于read()方法是阻塞方法,服务器端无法知道客户端是否终止输出,所以需要在客户端结束输出的地方调用socket的shutdownOutput()方法,表示客户端终止输出了。
2.客户端和服务端
六、UDP网络通信
1.UDP协议的网络编程
public void sender() throws IOException { DatagramSocket socket = new DatagramSocket(); String str = "我是UDP方式发射的导弹"; byte[] data = str.getBytes(); InetAddress inet = InetAddress.getByName("127.0.0.1"); DatagramPacket packet = new DatagramPacket(data, 0, data.length, inet, 9090); socket.send(packet); socket.close(); } //接收端 @Test public void receiver() throws IOException { DatagramSocket socket = new DatagramSocket(9090); byte[] buffer = new byte[100]; DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length); socket.receive(packet); System.out.println(new String(packet.getData(),0,packet.getLength())); socket.close(); }
public class UDP { @Test public void server() throws IOException { DatagramSocket socket = new DatagramSocket(10086); byte[] buffer = new byte[100]; InetAddress clientAddress = InetAddress.getByName("www.baidu.com"); int clientPort = 1; DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length,clientAddress,clientPort); socket.receive(packet); System.out.println(packet.getAddress() + " " + packet.getPort()); System.out.println( "收到的信息是: "+new String(packet.getData(),0,packet.getLength())); String s = new String(packet.getData()); s = s.toUpperCase(); DatagramPacket packet1 = new DatagramPacket(s.getBytes(), 0, s.getBytes().length,packet.getAddress(),packet.getPort()); socket.send(packet1); socket.close(); } @Test public void client() throws IOException { DatagramSocket socket = new DatagramSocket();//这个socket和客户的ip,port有隐式的绑定 String s = "abcabc"; byte[] message = s.getBytes(StandardCharsets.UTF_8); InetAddress inet = InetAddress.getByName("127.0.0.1"); DatagramPacket packet = new DatagramPacket(message, 0, message.length, inet, 10086); socket.send(packet); socket.receive(packet); System.out.println(new String(packet.getData(),0,packet.getLength())); socket.close(); } }
七、URL编程
1.URL:统一资源定位符,对应着互联网的某一资源地址
2.格式:
http://localhost:8080/examples/beauty.jpg?username=Tom
协议 主机名 : 端口 文件名 片段名 参数列表
3、URL类常用方法
public static void main(String[] args) { try { URL url = new URL("http://localhost:8080/examples/beauty.jpg?username=Tom"); System.out.println(url.getProtocol()); System.out.println(url.getHost()); System.out.println(url.getPort()); System.out.println(url.getPath()); System.out.println(url.getFile()); System.out.println(url.getQuery()); } catch (MalformedURLException e) { e.printStackTrace(); } }
输出结果:
http localhost 8080 /examples/beauty.jpg /examples/beauty.jpg?username=Tom username=Tom
从Tomcat服务器上下载文件:
public void test() throws IOException { URL url = new URL("http://localhost:8080/examples/beauty.jpg"); HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); urlConnection.connect(); InputStream is = urlConnection.getInputStream(); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("图片.jpg")); byte[] buffer = new byte[10]; int len; while ((len = is.read(buffer))!= -1){ bos.write(buffer,0,len); } bos.close(); is.close(); urlConnection.disconnect(); }
第十五章、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.toString()); p1.show(); //在Person类的外部,不可以通过Person类的对象调用其内部私有结构 //比如:私有的name,showNation()以及私有的构造器 } //反射之后,对于Person的操作 @Test public void test2() throws Exception{ Class clazz = Person.class; //1.通过反射,创建Person类的对象(先得到构造器) Constructor cons = clazz.getConstructor(String.class,int.class); Object obj = cons.newInstance("Tom", 12); Person p = (Person ) obj; System.out.println(p.toString()); //2.通过反射,调用对象指定的属性、方法 Field age = clazz.getDeclaredField("age"); age.set(p,10); System.out.println(p); //3.调用方法 Method show = clazz.getDeclaredMethod("show"); show.invoke(p); //通过反射,可以调用Person类的私有结构,比如私有的构造器、方法、属性 //调用私有的构造器 Constructor cons1 = clazz.getDeclaredConstructor(String.class); cons1.setAccessible(true); Person p1 = (Person) cons1.newInstance("Jerry"); System.out.println(p1); //调用私有的属性 Field name = clazz.getDeclaredField("name"); name.setAccessible(true); name.set(p1,"cg"); System.out.println(p1); //调用私有的方法 Method showNation = clazz.getDeclaredMethod("showNation", String.class); showNation.setAccessible(true); String nation = (String) showNation.invoke(p1, "中国"); System.out.println(nation); } }
//疑问1:通过直接new的方式或反射的方式都可以调用公共的结构,开发中到底用哪个? //建议:直接new对象 //什么时候会使用反射的方式呢? 反射具有动态性,动态需求中需要用反射
//疑问2:反射机制与面向对象的封装性是不是矛盾的?如何看待两个技术?
不矛盾,封装性是告诉我们哪些结构不建议我们在外部使用,但是你非要使用,就可以用反射,反射代表能不能用的问题。
二、理解Class类并获取Class实例
1.关于java.lang.Class类的理解:
(1)类的加载过程:
程序经过javac.exe命令之后,会生成一个或多个字节码文件(.class结尾)。一个java类对应一个文件
==接着我们使用java.exe命令对某个字节码文件进行解释运行。相当于把某个字节码文件加载到内存中,此过程就称为类的加载。加载到内存中的类,我们就称为运行时类,此运行时类,就作为Class的一个实例。==
(2)换句话说,Class的实例就对应着一个运行时类。
//万事万物皆对象?
对象.xxx,File,URL,反射,前端,数据库操作
(3)加载到内存中的运行时类,会缓存一定的时间。在此时间之内,我们可以通过不同的方式来获取此运行时类
2.获取Class的实例的方式(注:是获取不是创建,运行时类加载后就已经存在)
(前三种方式需要掌握)
public void test3() throws ClassNotFoundException { ///获取Class的实例的方式 //方式一:调用运行时类的属性:.class Class clazz1 = Person.class; System.out.println(clazz1); //方式二:通过运行时类的对象,调用getClass() Person p1 = new Person(); Class clazz2 = p1.getClass(); System.out.println(clazz2); //方式三:调用Class的静态方法:forName(String classPath) Class clazz3 = Class.forName("com.cg.java.Person"); System.out.println(clazz3); System.out.println(clazz1 == clazz2 ); System.out.println(clazz1 == clazz3); //方式四:使用类的加载器:ClassLoader(了解) ClassLoader classLoader = ReflectionTest.class.getClassLoader(); Class clazz4 = classLoader.loadClass("com.cg.java.Person"); System.out.println(clazz4); System.out.println(clazz1 == clazz4); }
3.Class实例可以是哪些结构?
三、类的加载过程与ClassLoader的理解
1.类的加载过程
类加载时只有静态的属性会被初始化赋值。非静态的属性不会赋值
2.ClassLoader的理解
public void test(){ //对于自定义类,使用系统类加载器进行加载 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader); //调用系统类加载器的getParent():获取扩展类加载器 ClassLoader classLoader1 = classLoader.getParent(); System.out.println(classLoader1); //调用扩展类加载器的getParent():无法获取引导类加载器 //引导类加载器主要负责加载java的核心类库,无法加载自定义类。 ClassLoader classLoader2 = classLoader1.getParent(); System.out.println(classLoader2);//null }
使用ClassLoader加载配置文件:关键方法getResourceAsStream("xxx.properties");
public void test2() throws IOException { /* Properties:用来读取配置文件 */ Properties pros = new Properties(); //读取配置文件的方式一: //此时的文件默认在当前的module下 // FileInputStream fis = new FileInputStream("jdbc.properties"); // pros.load(fis); //读取配置文件的方式二:使用ClassLoader //配置文件默认识别为:当前module的src下 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); InputStream is = classLoader.getResourceAsStream("jdbc1.properties"); pros.load(is); String user = pros.getProperty("user"); String age = pros.getProperty("age"); System.out.println(user + " "+age); }
四、创建运行时类的对象
1.通过反射创建对应的运行时类的对象
创建对象是一定需要通过构造器的,所谓的调用方法创建实例也只是内部调用了构造器而已
public void test() throws ClassNotFoundException, InstantiationException, IllegalAccessException { Class<Person> clazz = Person.class; /* newInstance():调用此方法,创建对应的运行时类的对象。内部调用了运行时类的空参构造器 要想此方法正常的创建运行时类的对象,要求: 1.运行时类必须提供空参的构造器 2.空参的构造器的访问权限得够。通常设置为public 在javabean中,要求提供一个public的空参构造器,原因: 1.便于通过反射,创建运行时类的对象 2.便于子类继承此运行时类时,默认调用super()时,保证父类有此构造器 */ Person person = clazz.newInstance(); System.out.println(person); }
也可以得到带参的构造器进而创建对象,但是这样用的很少,绝大部分是Class类实例直接调用newInstance();
public void test1() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { Class clazz = Class.forName("com.cg.java.Person"); Constructor constructor = clazz.getDeclaredConstructor(String.class, int.class); Person cg = (Person) constructor.newInstance("cg", 21); System.out.println(cg); }
通过随机创建运行时类的对象来体会反射的动态性
public void test2() throws ClassNotFoundException, InstantiationException, IllegalAccessException { for (int i = 0; i < 10; i++) { String pathName = null; int num = new Random().nextInt(3);//0,1,2 switch (num){ case 0: pathName = "java.lang.Object"; break; case 1: pathName = "java.util.Date"; break; case 2: pathName = "com.cg.java.Person"; break; } Object obj = getInstance(pathName); System.out.println(obj); } } public Object getInstance(String pathName) throws ClassNotFoundException, InstantiationException, IllegalAccessException { Class<?> clazz = Class.forName(pathName); Object obj = clazz.newInstance(); return obj; }
这里我们可以发现,传入不同的pathName就会返回不同类型的对象实例,这是框架的基础,框架中大量运用了反射的概念,我们一定要弄清楚。
五、获取运行时类的完整结构
1.获取当前运行时类的属性结构
public void test1(){ Class clazz = Person.class; //getFields():获取当前运行时类及其父类中声明为public访问权限的属性 Field[] fields = clazz.getFields(); for(Field field : fields){ System.out.println(field); } System.out.println(); //getDeclaredFields():获取当前运行时类中声明的所有属性。(不包含父类中声明的属性) Field[] declaredFields = clazz.getDeclaredFields(); for(Field field : declaredFields){ System.out.println(field); } } //获取属性的权限修饰符、数据类型、变量名。非静态属性不会在加载时赋值,所以暂时不考虑值 @Test public void test2(){ Class clazz = Person.class; Field[] declaredFields = clazz.getDeclaredFields(); for(Field field :declaredFields){ //1.权限修饰符 int modifiers = field.getModifiers(); System.out.print(Modifier.toString(modifiers) + "\t"); //2.数据类型 Class<?> type = field.getType(); System.out.print(type.getName( ) + "\t") ; //3.变量名 String name = field.getName(); System.out.println(name); } }
2.获取运行时类的方法结构
注:方法的注解只有是生命周期为RUNTIME的才能拿到
@Test public void test(){ Class<Person> clazz = Person.class; //getMethods():获取当前运行时类及其所有父类中声明为public权限的方法 Method[] methods = clazz.getMethods(); for(Method method : methods){ System.out.println(method); } System.out.println(); //getDeclaredMethods():获取当前运行时类中声明的所有方法(不包含父类中声明的) Method[] declaredMethods = clazz.getDeclaredMethods(); for(Method method : declaredMethods){ System.out.println(method); } } /* @Xxxx 权限修饰符 返回值类型 方法名(参数类型1 形参名1,...) throws XxxException{} */ @Test public void test1(){ Class clazz = Person.class; Method[] declaredMethods = clazz.getDeclaredMethods(); for(Method method : declaredMethods){ //1.获取方法声明的注解 Annotation[] annotations = method.getAnnotations(); for(Annotation a : annotations){ System.out.print(a + "\t"); } //2.权限修饰符 int modifiers = method.getModifiers(); System.out.print(Modifier.toString(modifiers)+"\t" ); //3.返回值类型 System.out.print(method.getReturnType().getName() +"\t"); //4.方法名 System.out.print(method.getName() ); System.out.print("("); //5.形参列表 Class[] parameterTypes = method.getParameterTypes(); if (parameterTypes!=null && parameterTypes.length==0){ }else { for (int i = 0; i < parameterTypes.length; i++) { if (i == parameterTypes.length -1){ System.out.print(parameterTypes[i].getName() + " args_" + i); break; } System.out.print(parameterTypes[i].getName() + " args_" + i + ","); } } System.out.print(")"); //6.抛出的异常 Class<?>[] exceptionTypes = method.getExceptionTypes(); if(exceptionTypes!=null && exceptionTypes.length!=0){ System.out.print("throws "); for (int i = 0; i < exceptionTypes.length; i++) { if (i == exceptionTypes.length-1){ System.out.print(exceptionTypes[i].getName()); break; } System.out.print(exceptionTypes[i].getName() + ","); } } System.out.println(); } }
自己写的代码:
@Test public void test() throws ClassNotFoundException { //用反射获取Person类中方法的信息 Class<?> clazz = Class.forName("com.cg.java1.Person"); Method[] declaredMethods = clazz.getDeclaredMethods(); for(Method method : declaredMethods){ // System.out.println(method); //注解 Annotation[] annotations = method.getAnnotations(); for(Annotation annotation : annotations){ System.out.print(annotation + "\t"); } //权限修饰符 String s = Modifier.toString(method.getModifiers()); System.out.print(s +"\t"); //返回值类型 Class<?> returnType = method.getReturnType(); System.out.print(returnType.getName()+"\t"); //方法名 String name = method.getName(); System.out.print(name+"("); //方法参数 Class<?>[] parameterTypes = method.getParameterTypes(); if (parameterTypes == null || parameterTypes.length== 0){ }else { for (int i = 0; i < parameterTypes.length; i++) { if (i == parameterTypes.length-1){ System.out.print(parameterTypes[i].getName() + " args_" + i ); }else System.out.print(parameterTypes[i].getName() + " args_" + i + ", "); } } System.out.print(")"); //异常 Class<?>[] exceptionTypes = method.getExceptionTypes(); if (exceptionTypes!=null && exceptionTypes.length!= 0){ System.out.print("throws "); for (int i = 0; i < exceptionTypes.length; i++) { if (i == exceptionTypes.length-1){ System.out.print(exceptionTypes[i].getName() ); }else System.out.print(exceptionTypes[i].getName() + ","); } } System.out.println(); } }
3。获取其他结构
/* 获取构造器结构 */ @Test public void test(){ Class clazz = Person.class; //getConstructors():获取当前运行时类中声明为public的构造器 Constructor[] constructors = clazz.getConstructors(); for(Constructor constructor:constructors){ System.out.println(constructor); } System.out.println(); //getDeclaredConstructor():获取当前运行时类中声明的所有构造器 Constructor[] declaredConstructors = clazz.getDeclaredConstructors(); for(Constructor constructor:declaredConstructors){ System.out.println(constructor); } } /* 获得运行时类的父类 */ @Test public void test2(){ Class clazz = Person.class; Class superclass = clazz.getSuperclass(); System.out.println(superclass); } /* 获得运行时类带泛型的的父类 */ @Test public void test3(){ Class clazz = Person.class; Type genericSuperclass = clazz.getGenericSuperclass(); System.out.println(genericSuperclass); } /* 获得运行时类带泛型的的父类的泛型 */ @Test public void test4(){ Class clazz = Person.class; Type genericSuperclass = clazz.getGenericSuperclass(); ParameterizedType paramType = (ParameterizedType) genericSuperclass; //获取泛型类型 Type[] actualTypeArguments = paramType.getActualTypeArguments(); System.out.println(actualTypeArguments[0].getTypeName()); } /* 获取运行时类实现的接口 */ @Test public void test5(){ Class clazz = Person.class; //getInterfaces()只能返回运行时类自己本身实现的接口,父类的得不到 Class[] interfaces = clazz.getInterfaces(); for( Class c : interfaces){ System.out.println(c); } System.out.println(); //先获取父类,再获得父类的接口 Class superclass = clazz.getSuperclass(); Class[] interfaces1 = superclass.getInterfaces(); for(Class c : interfaces1){ System.out.println(c); } } /* 获取运行时类所在的包 */ @Test public void test6(){ Class clazz = Person.class; Package aPackage = clazz.getPackage(); System.out.println(aPackage); } /* 获取运行时类的注解 */ @Test public void test7(){ Class clazz = Person.class; Annotation[] annotations = clazz.getAnnotations(); for(Annotation annotation : annotations){ System.out.println(annotation); } }
对于运行时类的所有属性、方法、构造器
-
属性、方法:
-
调用getFields() / getMethods() : 获取该运行时类及其父类的所有声明为public的属性/方法
-
调用getDeclaredFields() / getDeclaredMethods() : 获取该运行时类所有权限的属性/方法
-
-
构造器:
-
调用getConstructors() : 获取该运行时类所有声明为public的构造器
-
调用getDeclaredConstructors() : 获取该运行时类所有权限的构造器
-
六、调用运行时类的指定结构
/* 调用运行时类中指定的结构:属性、方法、构造器 */ public class ReflectTest { /* 不够理想,不需要掌握 */ @Test public void testField() throws NoSuchFieldException, InstantiationException, IllegalAccessException { Class clazz = Person.class; //创建运行时类的对象 Person p =(Person) clazz.newInstance(); //获取指定的属性 :要求运行时类中属性声明为public //所以通常不采用getField() Field id = clazz.getField("id"); //设置当前属性的值 //set():参数1:指明设置哪个对象的属性 参数2:将此属性值设置为多少 id.set(p,1001); //获取当前属性的值 //get():参数1:获取哪个对象的当前属性值 int pId = (int)id.get(p); System.out.println(pId); } /* 如何操作运行时类的指定属性--需要掌握 */ @Test public void testField1() throws InstantiationException, IllegalAccessException, NoSuchFieldException { Class clazz = Person.class; 创建运行时类的对象 Person p = (Person)clazz.newInstance(); //1. getDeclaredField(String fieldName):获取运行时类中指定变量名的属性 Field name = clazz.getDeclaredField("name"); //2. 保证当前属性是可访问的 name.setAccessible(true); //3.获取、设置指定对象的此属性值 name.set(p,"Tom"); String s = (String) name.get(p); System.out.println(s); } /* 如何操作运行时类的指定方法--需要掌握 */ @Test public void testMethod() throws Exception { Class clazz = Person.class; //创建运行时类的对象 Object o = clazz.newInstance(); Person p = (Person) o; /* 1.获取指定的某个方法 getDeclaredMethod():参数1:指明获取的方法的名称 参数2:指明获取的方法的形参 */ Method show = clazz.getDeclaredMethod("show", String.class); //2.保证当前方法是可访问的 show.setAccessible(true); /* 3.invoke():参数1:方法的调用者 参数2:给方法形参赋值的实参 invoke()的返回值即为对应类中调用的方法的返回值 */ Object obj = show.invoke(p, "CHN"); System.out.println(obj); System.out.println("*******如何调用静态方法**********"); //private static void showDesc() Method showDesc = clazz.getDeclaredMethod("showDesc"); showDesc.setAccessible(true); //如果调用的运行时类中的方法没有返回值,则此invoke()返回null Object invoke = showDesc.invoke(Person.class);//这里还能填null,因为clazz已经指明了是哪个类的静态方法 System.out.println(invoke);//null } /* 如何操作运行时类的指定构造器 */ @Test public void testConstructor() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { Class clazz = Person.class; //private Person(String name) /* 1.获取指定的构造器 getDeclaredConstructor():参数:指明构造器的参数列表 */ Constructor constructor = clazz.getDeclaredConstructor(String.class); //2.保证此构造器是可访问的 constructor.setAccessible(true); //3.调用此构造器创建运行时类的对象 Person person = (Person) constructor.newInstance("Tom"); System.out.println(person); } }
总结:创建类的对象的方式
方式一: new+构造器
方式二:要创建Xxx类的对象,可以考虑:Xxx、Xxxs、XxxFactory、XxxBuilder类中查看是否有静态方法的存在。可以调用其静态方法创建Xxx对象。
方式三:通过反射
七、反射的应用:动态代理
1.静态代理举例
特点:代理类和被代理类在编译期间,就确定下来了。
//代理类和被代理类实现的共同接口 interface ProxyFactory{ void produceCloth(); } //被代理类 class NikeFactory implements ProxyFactory{ @Override public void produceCloth() { System.out.println("开始生成耐克服装"); } } //代理类 class ClothFactory implements ProxyFactory{ ProxyFactory factory; public ClothFactory(){} public ClothFactory(ProxyFactory factory){ this.factory = factory; } @Override public void produceCloth() { System.out.println("工厂开始生产"); factory.produceCloth(); // 代理类调用该方法时也调用了被代理类的方法 System.out.println("工厂结束生产"); } } public class StaticProxyTest { @Test public void test(){ NikeFactory nikeFactory = new NikeFactory(); ClothFactory clothFactory = new ClothFactory(nikeFactory); clothFactory.produceCloth(); } }
2.动态代理举例:
动态代理:通过生产代理类的工厂 为程序提供的被代理类,动态的创建一个代理类。
interface Human{ String getBelief(); void eat(String food); } //被代理类 class SuperMan implements Human{ @Override public String getBelief() { return "I belief I can fly"; } @Override public void eat(String food) { System.out.println("我喜欢吃" + food); } } /* 要想实现动态代理,需要解决的问题? 问题1:如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象 问题2:当通过代理类的对象调用方法时,如何动态的去调用被代理类中的同名方法 */ class ProxyFactory{ //调用此方法,返回一个代理类的对象,解决问题1 public static Object getProxyInstance(Object obj){//obj:被代理类的对象 MyInvocationHandler hander = new MyInvocationHandler(); hander.bind(obj); Object o = Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), hander); return o; } } class MyInvocationHandler implements InvocationHandler{ private Object obj;//需要使用被代理类的对象赋值 public void bind(Object obj){ this.obj = obj; } //当我们通过代理类的对象,调用方法a时,就会自动的调用如下的方法:invoke() //将被代理类要执行的方法a的功能 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //method:即为代理类对象调用的方法,此方法也就作为了被代理类对象调用的方法 Object returnValue = method.invoke(obj, args); //上述方法的返回值就作为当前类中的invoke()方法的返回值 return returnValue; } } public class ProxyTest { public static void main(String[] args) { SuperMan superMan = new SuperMan(); //proxyInstance:代理类的对象 Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan); //当通过代理类对象调用方法时,会自动的调用被代理类中同名的方法 proxyInstance.getBelief(); proxyInstance.eat("四川麻辣烫"); NikeClothFactory nikeClothFactory = new NikeClothFactory(); ClothFactory proxyInstance1 = (ClothFactory) ProxyFactory.getProxyInstance(nikeClothFactory); proxyInstance1.produceCloth(); } }
自己写的代码:
interface Human{ void eat(String food); String belief(String belief); } //被代理类 class Superman implements Human{ @Override public void eat(String food) { System.out.println("我爱吃" + food); } @Override public String belief(String belief) { return "我的信仰是" + belief; } } //代理工厂,用来动态根据被代理类,生成代理类 class ProxyFactory{ Object getInstance(Object t){ Object o = Proxy.newProxyInstance(t.getClass().getClassLoader(), t.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object returnValue = method.invoke(t, args); return returnValue; } }); return o; } } public class ProxyTest { @Test public void test(){ Superman superman = new Superman(); ProxyFactory proxyFactory = new ProxyFactory(); Human instance = (Human)proxyFactory.getInstance(superman); String belief = instance.belief("吃饭"); System.out.println(belief); instance.eat("主任"); } }
3.动态代理与AOP
即,在写死了的方法间插入动态的方法
第十六章、Java8的其他新特性
之前提到过的新特性:
-
接口中可以定义默认方法和静态方法
-
新的日期时间API
-
注解 类型注解、 可重复注解
-
集合类在添加数据时才造数组,HashMap底层结构
java8新特性概述
一、Lambda表达式
1.Lambda表达式举例
@Test public void test1(){ Runnable r1 = new Runnable() { @Override public void run() { System.out.println("我爱上海"); } }; r1.run(); System.out.println("***********"); //Lambda表达式的写法 Runnable r2 = () -> System.out.println("我爱上海外滩"); r2.run(); } @Test public void test2(){ Comparator<Integer> com1 = new Comparator<>() { @Override public int compare(Integer o1, Integer o2) { return Integer.compare(o1,o2); } }; System.out.println(com1.compare(12, 21)); System.out.println("****************"); //Lambda表达式的写法 Comparator<Integer> com2 = (o1, o2) -> Integer.compare(o1,o2); System.out.println(com2.compare(32, 21)); System.out.println("****************"); //方法引用 Comparator<Integer> com3 = Integer :: compare; System.out.println(com2.compare(32, 21)); }
2.Lambda表达式的使用
(1)举例:(o1,o2) -> Integer.compare(o1,o2);
(2)格式:
-
->: lambda操作符 或 箭头操作符
-
->左边:lambda形参列表(其实就是接口中的抽象方法的形参列表)
-
->右边:lambda体(其实就是重写的抽象方法的方法体)
(3)lambda表达式的使用:(分为六种情况介绍)
总结:
-
->左边:lambda形参列表的参数类型(有类型推断)可以省略;如果lambda形参列表只有一个参数,其()也可省略
-
->右边:lambda体应该使用一对{}包裹;如果lambda体只有一条执行语句(可能是return语句) ,省略大括号和return关键字
(4)Lambda表达式的本质:作为函数式接口的一个实例(要求该接口只有一个抽象方法)
public class LambdaTest1 { //语法格式1:无参,无返回值 @Test public void test1(){ Runnable r1 = new Runnable() { @Override public void run() { System.out.println("我爱上海"); } }; r1.run(); System.out.println(); Runnable r2 = () -> System.out.println("i am a pig!"); r2.run(); } //语法格式2:lambda需要一个参数,无返回值 @Test public void test2(){ Consumer<String> con = new Consumer<String>() { @Override public void accept(String s) { System.out.println(s); } }; con.accept("谎言和誓言"); System.out.println("*****************"); Consumer<String> con1 = (String s) -> { System.out.println(s); }; con1.accept("一个是听得人当真"); } //语法格式3:数据类型可以省略,因为可由编译器推断得出,称为"类型推断" @Test public void test3(){ Consumer<String> con1 = (String s) -> { System.out.println(s); }; con1.accept("一个是听得人当真"); System.out.println("**************"); Consumer<String> con2 = (s) -> { System.out.println(s); }; con2.accept("一个是听得人当真"); } //语法格式4:lambda若只需要一个参数时,参数的小括号可以省略 @Test public void test4() { Consumer<String> con1 = (String s) ->{ System.out.println(s); }; con1.accept("一个是听得人当真"); System.out.println("**************"); Consumer<String> con2 = s -> { System.out.println(s); }; con2.accept("一个是听得人当真"); } //语法格式五:Lambda需要两个或以上的参数,多条执行语句,并且可以有返回值 @Test public void test6(){ Comparator<Integer> com1 = new Comparator<>() { @Override public int compare(Integer o1, Integer o2) { System.out.println(o1); System.out.println(o2); return o1.compareTo(o2); } }; System.out.println(com1.compare(12, 21)); System.out.println("*********"); Comparator<Integer> com2 = (o1,o2) -> { System.out.println(o1); System.out.println(o2); return o1.compareTo(o2); }; } //语法格式六:当Lambda体只有一条语句时,return与大括号若有,都可以省略 @Test public void test7(){ Comparator<Integer> com1 = (o1,o2) -> o1.compareTo(o2); System.out.println(com1.compare(12, 21)); } @Test public void test8(){ Consumer<String> con = s -> System.out.println("i am a pig"); } }
二、函数式(Functional)接口
1.如果一个接口中,只声明了一个抽象方法,那么就是函数式接口。
2.我们可以在接口上声明注解@FunctionalInterface,验证它是否是一个函数式接口
3.以前只有一个抽象方法的接口 的匿名实现类都可以用lambda表达式来写
4.java内置的四大核心函数式接口
消费型接口:Consumer< T > void accept(T t)
供给型接口:Supplier< T > T get()
函数型接口:Function<T,R> R apply(T t)
断定型接口:Predicate< T > boolean test(T t)
三、方法引用与构造器引用
(一)、方法引用的使用
1.使用情境:当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用
2.方法引用,本质上就是Lambda表达式,而Lambda表达式作为函数接口的实例,所以方法引用,也是函数式接口的实例
3.使用格式: 类(对象) : : 方法名
4.具体分为如下的三种情况:
对象 : : 非静态方法
类 : : 静态方法
类 : : 非静态方法
5.方法引用使用的要求:
要求接口中的抽象方法的形参列表和返回值类型与方法引用的形参列表和返回值类型相同。(针对于情况1和情况2)
//情况一:对象::实例方法 //Consumer中的void accept(T t) //PrintStream中的void println(T t) @Test public void test1(){ Consumer<String> con1 = s -> System.out.println(s); con1.accept("猪"); System.out.println("**********"); Consumer<String> con2 = System.out :: println; con2.accept("猪"); } //Supplier中的T get() //Employee中的String getName() @Test public void test2(){ Supplier<String> sup1 = () -> "123"; String s = sup1.get(); System.out.println(s); System.out.println("************"); Supplier<String> sup2 = new Person("Tom") :: getName; String s1 = sup2.get(); System.out.println(s1); } //情况2:类 :: 静态方法 //Comparator中的int compare(T t1,T t2) //Integer 中的 int compare(T t1,T t2) @Test public void test3(){ Comparator<Integer> com1 = (i1,i2) -> i1.compareTo(i2); System.out.println(com1.compare(1, 2)); Comparator<Integer> com2 = Integer :: compare; System.out.println(com2.compare(1, 2)); } //Function 中的R apply(T t) //Math中的Long round(Double d) @Test public void test4(){ Function<Double , Long> fun1 = (a) -> Math.round(a); System.out.println(fun1.apply(1.2)); System.out.println("************"); Function<Double,Long> fun2 = Math :: round; System.out.println(fun2.apply(1.222)); } //情况3:类 :: 实例方法 (有难度) //Comparator中的int compare(T t1, T t2) //String 中的 int t1.compare(t2) @Test public void test5(){ Comparator<String > com1 = (s1,s2) -> s1.compareTo(s2); System.out.println(com1.compare("ac", "av")); System.out.println("**************"); Comparator<String> com2 = String :: compareTo; System.out.println(com2.compare("aa","bb")); } //BiPredicate中的boolean test(T t1,T t2); //String中的boolean t1.equals(t2) @Test public void test6(){ BiPredicate<String,String> bip = String ::equals; System.out.println(bip.test("aa", "aa")); BiPredicate<String,String> bip1 = (s1,s2) -> s1.equals(s2); System.out.println(bip1.test("aa","bb")); } //Function中的R apply(T t) //Employee中的String getName() @Test public void test7(){ Function<Person,String> fun = s -> s.getName(); System.out.println(fun.apply(new Person("Tom"))); System.out.println("*****************"); Function<Person,String> fun1 = Person::getName; System.out.println(fun1.apply(new Person("Tom"))); }
(二)、构造器引用
和方法引用类似,函数式接口的抽象方法的形参列表和构造器的形参列表一致。抽象方法的返回值类型即为构造器所属的类的类型
//构造器引用 //Supplier中的T get() @Test public void test1(){ Supplier<Person> sup1 = () -> new Person("Tom"); Person person = sup1.get(); System.out.println(person); System.out.println("********8"); Supplier<Person> sup2 = Person :: new; System.out.println(sup2.get()); } //Function 中的R apply(T t) @Test public void test2(){ Function<String , Person> fun1 = s -> new Person(s); Person apply = fun1.apply("123"); System.out.println(apply.getName()); System.out.println("********8"); Function<String,Person> fun2 = Person::new; System.out.println(fun2.apply("1234").getName()); } //BiFunction 中的 R apply(T t,U u) @Test public void test3(){ BiFunction<Integer,String,Person> fun1 = (i,s) -> new Person(i,s); System.out.println(fun1.apply(12, "Tom")); BiFunction<Integer,String,Person> fun2 = Person::new; System.out.println(fun2.apply(12, "Jerry")); }
(三)、数组引用
大家可以把数组看作是一个特殊的类,则写法与构造器引用一致。
//数组引用 //Function中的 R apply(T t) @Test public void test4(){ Function<Integer,String[]> fun1 = i -> new String[i]; String[] apply = fun1.apply(10); for(String s : apply){ System.out.println(s + "11"); } Function<Integer,String[]> fun2 = String[] :: new ; for(String s : fun2.apply(5)){ System.out.println(s + "22"); } }
lambda表达式很大的作用是把表达式写成接口实现类的特质
四、强大的Stream API
具有不可变性的:
String类,LocalDateTime之类,Stream
1.Stream关注的是对数据的计算,与CPU打交道 * 集合关注的是数据的存储,与内存打交道 * 2.(1)Stream自己不会存储元素 * (2)Stream不会改变源对象。相反,他们会返回一个持有结果的新Stream * (3)Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行 * 3.Stream执行流程 * (1)Stream的实例化 * (2)一系列的中间操作(过滤,映射、。。。) * (3)终止操作 * 4.说明: * (1)一个中间操作链,对数据源的数据进行处理 * (2)一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用
Stream的几个易错的地方:
Stream具有不可变性,返回的是一个被处理的新的Stream;
一个Stream对象执行了终止操作之后,该对象就不能再被使用
1.Stream的实例化
//测试Stream的创建 //创建Stream方式一:通过集合 @Test public void test1(){ List<String> list =new ArrayList<>(); list.add("123"); list.add("aa"); //default Stream<T> stream():Collection中的默认方法,返回一个顺序流 Stream<String> stream = list.stream(); //default Stream<T> parallelStream():Collection中的默认方法,返回一个并行流 Stream<String> stringStream = list.parallelStream(); } //创建Stream方式二:通过数组 @Test public void test2(){ //调用Arrays类的static <T> Stream<T> stream(T[] array):返回一个流 Stream<String> stream = Arrays.stream(new String[10]); IntStream stream1 = Arrays.stream(new int[]{1, 2, 3}); } //创建Stream方式三:通过Stream的of() @Test public void test3(){ //调用Stream类的静态方法of() Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5); } //创建Stream方式四:创建无限流 @Test public void test4(){ //迭代 //遍历前10个偶数 Stream<Integer> stream = Stream.iterate(0, t -> t + 2); stream.limit(10).forEach(System.out :: println); //生成 Stream<Double> stream1 = Stream.generate(() -> Math.random()); stream1.limit(10).forEach(System.out::println); }
2.Stream的中间操作
(1)筛选与切片
//1.筛选与切片 @Test public void test1(){ //filter(Predicate p) --接收lambda,从流中排除某些元素 List<String> list = Arrays.asList("北京","天津","南京1"); Stream<String> stream = list.stream(); Stream<String> stream1 = stream.filter(s -> s.contains("京")); stream1.forEach(System.out :: println); //limit(n)--截断流,使其元素不超过给定数量 list.stream().limit(2).forEach(System.out::println); //skip(n)--跳过元素,返回一个扔掉了前n个元素的流,若流中元素不足n个,则返回空流,与limit(0)相同 Stream.of(1,2,3,4,5,6,7).skip(3).forEach(s-> System.out.println(s)); //distinct()--筛选,通过流所生成元素的hashCode()和equals()去除重复元素 Stream.of(1,2,3,3,4,2,9,6,7,4,3).distinct().forEach(System.out::println); }
(2)映射
//2.映射 @Test public void test2(){ //map(Function f)--接收一个函数作为参数,讲元素转换成其他形式或提取信息 // ,该函数会被应用到每个元素上,并将其映射成一个新的元素。 List<String> list = Arrays.asList("aa", "bb", "cc"); list.stream().map(s->s.toUpperCase()).forEach(s-> System.out.println(s)); //练习1:获取员工姓名长度大于3的员工的姓名 Person p1 = new Person("Tom", 12); Person p2 = new Person("Jerry",23); Person p3 = new Person("hiee",23); List<Person> list1 = Arrays.asList(p1,p2,p3); Stream<Person> stream = list1.stream().filter(p-> p.name.length()>3 ); stream.forEach( p -> System.out.println(p.name)); System.out.println(); //flatMap(Function f)--接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流 //flatMap的主要功能在于将多个流的元素连接到一起,这是map(Function f)很难做到的 list1.stream().flatMap(p -> StreamAPITest1.getStreamFromString(p.name)).forEach(System.out::println); //如果用map(Function f)写 list1.stream().map(p->StreamAPITest1.getStreamFromString(p.name)).forEach(s->s.forEach(System.out::println)); } public static Stream<Character> getStreamFromString(String s){ ArrayList<Character> characters = new ArrayList<>(); for(Character c : s.toCharArray()){ characters.add(c); } return characters.stream(); }
(3)排序
//3.排序 @Test public void test3(){ //sorted()--自然排序 List<Integer> list = Arrays.asList(1, 2, 42, 531, 23, 256, 234, 23, 5); list.stream().sorted().forEach(System.out::println); //用sorted()给没有实现Comparable接口的自定义类排序,会报错 //sorted(Comparator com)--定制排序 Person p1 = new Person("Tom", 12); Person p2 = new Person("Jerry",24); Person p3 = new Person("hiee",23); List<Person> list1 = Arrays.asList(p1,p2,p3); list1.stream().sorted((p,q) -> p.name.compareTo(q.name)).forEach(System.out::println); list1.stream().sorted((p,q)->Integer.compare(p.age,q.age)).forEach(System.out::println); }
3.Stream的终止操作
(1)匹配与查找
//1-匹配与查找 @Test public void test1(){ //allMatch(Predicate p)--检查是否匹配所有元素。练习:是否所有的员工的年龄都大于18 Person p1 = new Person("Tom", 21); Person p2 = new Person("Jerry", 11); Person p3 = new Person("Smith", 16); Person p4 = new Person("Dog", 75); List<Person> list = Arrays.asList(p1,p2,p3,p4); System.out.println(list.stream().allMatch(p -> p.age > 18)); //anyMatch(Predicate p)--检查是否至少匹配一个元素 System.out.println(list.stream().anyMatch(p -> p.name.equals("Tom"))); //noneMatch(Predicate p)--检查是否没有匹配的元素。 System.out.println(list.stream().noneMatch(p -> p.age == 20)); //findFirst--返回第一个元素 System.out.println(list.stream().findFirst()); //findAny--返回当前流中的任意元素 Optional<Person> any = list.parallelStream().findAny(); System.out.println(any); //count--返回流中元素的总个数 System.out.println(list.stream().filter(p->p.age>18).count()); //max(Comparator c)--返回流中的最大值 Optional<Integer> max = list.stream().map(p->p.age).max((i,j)->i.compareTo(j)); System.out.println(max); //min(Comparator c)--返回流中的最小值 Optional<Person> min = list.stream().min((p, q) -> Integer.compare(p.age, q.age)); System.out.println(min); //forEach(Consumer c)--内部迭代 list.stream().forEach(System.out::println); //使用集合的遍历操作 list.forEach(s-> System.out.println(s)); }
(2)归约
//2.归约 @Test public void test2(){ //reduce(BinaryOperator)--可以将流中元素反复结合起来,得到一个值,返回Optional<T> //练习1:计算1-10自然数的和 List<Integer> list = new ArrayList<>(); for (int i = 1; i <= 10; i++) { list.add(i); } System.out.println(list.stream().reduce((i,j)->i+j)); //reduce(T identity, BinaryOperator)--可以将流中元素反复结合起来,得到一个值, // 返回这个值再作用于identity,类型为T //练习1:计算1-10自然数的和 List<Integer> list1 = new ArrayList<>(); for (int i = 1; i < 10; i++) { list1.add(i); } System.out.println(list1.stream().reduce(10,(i,j)->i+j)); }
(3)收集
//3.收集 @Test public void test3(){ Person p1 = new Person("Tom", 21); Person p2 = new Person("Jerry", 11); Person p3 = new Person("Smith", 16); Person p4 = new Person("Dog", 75); List<Person> list = Arrays.asList(p1,p2,p3,p4); Stream<Person> filterStream = list.stream().filter(p -> p.age > 11); List<Person> list1= filterStream.collect(Collectors.toList()); list1.forEach(p-> System.out.println(p)); }
五、Optional类
Optional类:为了在程序中避免出现空指针异常而创建的
常用的方法:ofNullable(T t)
ofElse(T t)
of()和get()通常搭配使用
ofNullable()和orElse()搭配使用
//创建Optional类的方法 //1.of(T t ):要求非空 //2.empty():创建一个空的Optional //3.ofNullable(T t):t可以为null @Test public void test1(){ Optional<Girl> girl = Optional.of(new Girl()); System.out.println(girl); System.out.println(Optional.ofNullable(new Girl())); } //举例 @Test public void test2(){ // Boy boy = new Boy(); // System.out.println(getGirlName1(boy)); Girl girl = new Girl(); girl = null; Optional<Girl> girl1 = Optional.ofNullable(girl); //orElse(T t1):如果当前的Optional内部封装的t是非空的,则返回内部的t //如果内部的t是空的,则返回orElse(T t1)方法中的参数t1 Girl girl2 = girl1.orElse(new Girl("dilireba")); System.out.println(girl2); Boy boy = new Boy(); System.out.println(getGirlName2(boy)); Boy boy1 = new Boy(new Girl("胡伟")); System.out.println(getGirlName2(boy1)); Boy boy2 = null; System.out.println(getGirlName2(boy2)); } public String getGirlName(Boy boy){ return boy.getGirl().getName(); } //优化以后的getGirlName()方法 public String getGirlName1(Boy boy){ if(boy!= null){ if(boy.getGirl()!=null){ return boy.getGirl().getName(); } return null; } return null; } //使用Optional类优化以后的getGirlName()方法 public String getGirlName2(Boy boy){ Optional<Boy> boy1 = Optional.ofNullable(boy); Boy boyOption = boy1.orElse(new Boy(new Girl("赵丽颖"))); //此时boyOption不可能为空了 Girl girl = boyOption.getGirl(); Optional<Girl> girl1 = Optional.ofNullable(girl); //此时的girl2一定非空 Girl girl2 = girl1.orElse(new Girl("王汉生")); return girl2.getName(); }
第十七章、java9&java10&java11新特性
换jdk可以看康师傅p692
一、java9新特性
1.jdk目录结构的变化,将java8及以前单独放出来的jre 分开了,不再有jre文件夹
2.模块化系统
可以在两个模块的src目录下新建module-info来使用不同模块下的类,非常好用
exports,requires
3.java的REPL工具:jShell命令
4.接口中可以定义私有方法了
5.钻石操作符的功能添加
6.改进try语句
java8:可以将需要关闭的资源写在try后面一对小括号中,但是要求必须将资源的初始化写在括号里
java9:不需要再将初始化写在括号了,只需要把要关闭的资源的变量名写入即可
注:不可在大括号中修改资源变量
public static void main(String[] args) { //jdk7及写法 InputStreamReader isr = null; BufferedReader br = null; try { isr = new InputStreamReader(System.in); br = new BufferedReader(isr); String s = br.readLine(); System.out.println(s); } catch (IOException e) { e.printStackTrace(); } finally { try { br.close(); } catch (IOException e) { e.printStackTrace(); } try { isr.close(); } catch (IOException e) { e.printStackTrace(); } } } @Test public void test() { //jdk8: try(InputStreamReader isr = new InputStreamReader(System.in); BufferedReader br = new BufferedReader(isr)) { String s = br.readLine(); System.out.println(s); } catch (IOException e) { e.printStackTrace(); } //jdk9: InputStreamReader isr1 = new InputStreamReader(System.in); BufferedReader br1 = new BufferedReader(isr1); try(isr1;br1){ String s = br1.readLine(); System.out.println(s); } catch (IOException e) { e.printStackTrace(); } }
7.String存储结构变更
StringBuffer和StringBuilder也变了
8.集合工厂方法:创建只读集合
java9之前的写法:
public static void main(String[] args) { List<Integer> list = Arrays.asList(1, 2, 3); List<Integer> list1 = Collections.unmodifiableList(list); list1.add(1);//报错 //补充 list.add(1);//报错 这个要记住 list.set(1,3);//这种方式创建的集合可以修改,不能添加 }
java9新特性:集合工厂创建只读集合
ArrayList<Integer> list1 = List.of(1,2,3,4); list1.add(5);//报错
HashSet set = Set.of(1,2,3,4,5); Map map = Map.of("a",1,"b",2);
9.InputStream加强
10.增强的StreamAPI
11.Optional类中stream()的使用
12.Javascript引擎升级:Nashorn
二、Java10新特性
1.局部变量类型推断:
之前遇到的类型推断:数组、泛型、lambda
@Test public void test(){ //1.声明变量时,根据所赋的值,推断变量类型 var num = 10; var s = "123"; var list = new ArrayList<String>(); list.add("123"); //2.遍历操作 for(var i : list){ System.out.println(i); } //3.普通的遍历操作 for(var i = 0; i<100; i++){ System.out.println(i); } } @Test public void test2(){ //1.局部变量不赋值不能使用局部变量类型推断 // var num; //2.lambda表达式不能用var //3.方法引用不能用var //4.数组的静态初始化中 // var arr = {1,2,3}; }
2.集合新增创建不可变集合的方法
三、java11新特性
结束啦