Safe: 13-代码审计进阶
- TAGS: Safe
Java 开发基础
Java语言概述
Java语言发展史
Java之父:詹姆斯·高斯林(James Gosling)
1977年获得了加拿大卡尔加里大学计算机科学学士学位,1983年获得了美国卡内基梅隆大学计算机科学博士学位,毕业后到IBM工作,设计IBM第一代工作站NeWS系统,但不受重视。后来转至Sun公司,1990年,与Patrick,Naughton和Mike Sheridan等人合作“绿色计划”,后来发展一套语言叫做“Oak”,后改名为Java。
Sun公司
- 1995年java语言
- 1996年Java1.0
- 1997 Java1.1
- 1998 Java1.2
- 1998 Java1.2
- 2000 Java1.3
- 2002 Java1.4
- 2004 Java5.0
- 2006 Java6.0
2009年Oracle甲骨文公司收购Sun公司
- 2001 Java7.0
- 2014 Java8.0
- 2017 Java9.0
- 2018.3 Java10.0
- 2018.9 Java11.0
Java语言平台版本
- JavaSE(Java Platform Standard Edition)标准版
是为开发普通桌面和商务应用程序提供的解决方案 该技术体系是其他,可以完成一些桌面应用程序的开发两者的基础 - JavaME(Java Platform Micro Edition)小型版
是为开发电子消费产品和嵌入式设备提供的解决方案,已经被安卓 IOS - JavaEE(Java Platform Enterprise Edition)企业版
是为开发企业环境下的应用程序提供的一套解决方案
Java语言特点
- 简单性 面向对象 分布式
- 可移植性 (跨平台) 多线程 动态性
- 健壮性 安全性 开源
Java跨平台
什么是平台
通过Java语言编写的应用程序在不同的系统平台上都可以运行
跨平台的原理
只要在需要运行java应用程序的操作系统上,先安装一个Java虚拟机( JVM
Java Virtual Machine)即 可。由JVM来负责Java程序在该系统中的运行。
总结
因为有了JVM,所以同一个Java程序在三个不同的操作系统中都可以执行。这样就实现了Java程序的可移植性。也称为Java具有良好的跨平台性。
JDK、JRE、JVM
- JDK(Java Development Kit Java开发工具包)
JDK是提供给Java开发人员使用的,其中包含了java的开发工具,也包括了JRE。所以安装了JDK,就不用在单独安装JRE了。
其中的开发工具:编译工具(javac.exe) 执行工具(java.exe) 打包工具(jar.exe)等 - JRE(Java Runtime Environment Java运行环境)
包括Java虚拟机(JVM Java Virtual Machine)和Java程序所需的核心类库等,如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。 - JVM(Java Virtual Machine Java虚拟机)
整个java实现跨平台的最核心的部分,保证程序的跨平台性,以及编译执行写好的java程序!
总结
使用JDK开发完成的java程序,交给JRE去运行。由jvm保证跨平台性。
JDK、JRE、JVM 之间的关系
- JDK
- JRE
- JVM
- 核心类库
- JVM
- 开发工具
- JRE
总结
JDK 包含JRE,JRE包含JVM ,开发中我们只需要安装JDK即可!
JDK的下载安装
下载JDK
通过官方网站获取JDK http://www.oracle.com
注意:针对不同的操作系统,需要下载对应版本的JDK
安装JDK
傻瓜式安装,下一步即可。但默认的安装路径是在C:\Program Files下,为方便统一管理建议修改安装路径,将与 开发相关的软件都安装到一个目录下,例如:E:\develop。 注意:安装路径不要包含中文或者空格等特殊字符(使用纯英文目录)。
DOS命令
为什么学习DOS命令
在接触集成开发环境之前,我们需要使用命令行窗口对java程序进行编译和运行,所以需要知道一些常用DOS命 令。也为以后学习Linux等其他内容做一些准备!
如何打开DOS命令窗口
打开命令行窗口的方式:
- win + r 打开运行窗口,输入cmd,回车。
- 点击win窗口图标,输入cmd,选择命令提示符
常用的DOS命令
盘符名称: E:回车,表示切换到E盘。
dir: 查看当前路径下的内容。
cd 目录: 进入单级目录。cd JavaSE
cd.. : 回退到上一级目录。
cd 目录1\目录2…: 进入多级目录。
cd\或cd/: 回退到盘符目录。
cls: 清屏。
exit: 退出命令提示符窗口。
环境搭建
配置开发环境
为什么配置环境变量
开发Java程序,需要使用JDK提供的开发工具(比如javac.exe、java.exe等命令),而这些工具在JDK的安装目录的 bin目录下,如果不配置环境变量,那么这些命令只可以在该目录下执行。我们不可能把所有的java文件都放到JDK 的bin目录下,所以配置环境变量的作用就是可以使bin目录下的java相关命令可以在任意目录下使用。
配置步骤
首先右键 【此电脑】 —> 选择 【属性】 —> 点击左侧的 【高级系统设置】 —> 点击 【环境变量】 —> 在下方的 【系统变量】 中点击 【新建】
新建变量名为 【JAVA_HOME】 ,变量值我们点击 【浏览目录】 ,选择jdk的安装路径jdk1.8.0_121文件夹 然后点击 【确定】
找到path路径中写 %JAVA_HOME%\bin; ,放在最前面,不要忘了加英文的分号
测试开发环境
通过javac和java命令验证 注意 :如果提示 javac不是内部或者外部命令 ,原因是path配置错误, 重新按照上一步的步骤进行配置即可!
避免C:\WINDOWS\system32出现所有名字以java开头的文件,有,删之; 注意:jdk不要安装在带中文的路径下,最好也是不包含空格字符的英文路径;
HelloWorld案例
Java程序开发运行流程
开发Java程序,需要三个步骤:编写源程序,编译程序,运行程序
- Java源代码
- javac|编译
- Java字节码文件|HelloWorld.class
- java|执行
- 运行结果
编写HelloWorld程序
- 新建文本文档文件,修改名称为HelloWorld.java
- 用记事本打开HelloWorld.java文件,输写程序内容
public class HelloWorld { public static void main(String[] args) { System.out.println("HelloWorld"); } }
- HelloWorld案例的编译和运行
编译:javac 文件名.java 范例:javac HelloWorld.java // 出生成HelloWorld.class文件 执行:java 类名 范例:java HelloWorld
HelloWorld常见问题
- 大小写问题。Java语言对大小写敏感(区分大小写)
- 非法字符问题。Java中的符号都是英文格式的
- 在系统中显示文件的扩展名,避免出现HelloWorld.java.txt文件
- 编译命令后的java文件名需要带文件后缀.java
- 运行命令后的class文件名(类名)不带文件后缀.class
Java基础语法
注释
什么是注释
注释是对代码的解释和说明文字,可以提高程序的可读性,因此在程序中添加必要的注释文字十分重要
注释的分类
单行注释
单行注释的格式是使用//,从//开始至本行结尾的文字将作为注释文字
// 这是单行注释文字
多行注释
多行注释的格式是使用/* 和 */将一段较长的注释括起来
/* 这是多行注释文字 这是多行注释文字 这是多行注释文字 */ // 注意:多行注释不能嵌套使用。
文档注释
文档注释以 ** 开始,以 * 结束
/** 这是文档注释的内容 */
关键字
什么是关键字
关键字是指被java语言赋予了特殊含义的单词
关键字的特点
关键字的字母全部小写。
常用的代码编辑器对关键字都有高亮显示,比如现在我们能看到的public、class、static等。
常量
什么是常量
在程序运行过程中,其值不可以发生改变的量。
Java中常量的分类
- 字符串常量
用双引号括起来的多个字符(可以包含0个、一个或多个),例如"a"、"abc"、"中国"等 - 整数常量
整数,例如:-10、0、88等 - 小数常量
小数,例如:-5.5、1.0、88.88等 - 字符常量
用单引号括起来的一个字符,例如:'a'、'5'、'B'、'中'等 - 布尔常量
布尔值,表示真假,只有两个值true和false - 空常量
一个特殊的值,空值,值为null
常量演示
除空常量外,其他常量均可使用输出语句直接输出
/* 常量的分类: 字符串常量 "123" "abc" 整数常量: 100 1 -1 -100 小数常量: 1.1 1.2 -1.1 -1.2 字符常量: 'a' '1' '你' 字符'' 有且仅有一个字符 布尔常量: true false 空常量: null */ public class Demo01{ public static void main(String[] args){ // 字符串常量 "123" "abc" System.out.println("abc"); // 整数常量 System.out.println(100); System.out.println(-100); //小数常量 System.out.println(1.1); System.out.println(-1.1); // 字符常量 System.out.println('a'); System.out.println('你'); // System.out.println(''); System.out.println(' '); // 布尔常量: true false System.out.println(true); System.out.println(false); // null是不能直接打印,有特殊应用的,在后面的数组 集合 对象中使用 // System.out.println(null); } }
数据类型
计算机存储单元
我们知道计算机是可以用来存储数据的,但是无论是内存还是硬盘,计算机存储设备的最小信息单元叫“位(bit)”,我们又称之为“比特位”,通常用小写的字母”b”表示。而计算机中最基本的存储单元叫“字节(byte)”,通常用大写字母”B”表示,字节是由连续的8个位组成。
存储单位换算
除了字节外还有一些常用的存储单位,其换算单位如下
1B(字节) = 8bit 1KB = 1024B 1MB = 1024KB 1GB = 1024MB 1TB = 1024GB 1PB = 1024TB 1EB = 1024PB
Java中的数据类型
- 为什么有数据类型
Java是一个强类型语言,对每一种数据都规定了范围. - 数据类型的分类
- 基本数据类型Primitive Type
- 数值型
- 整数类型(byte, short, int, long)
- 浮点类型(float, double)
- 整数类型(byte, short, int, long)
- 字符型(char)
- 布尔型(boolean)
- 数值型
- 引用数据类型Refrence Type
- 类、枚举、注解
- 接口(interface)
- 数组([])
- 类、枚举、注解
- 基本数据类型Primitive Type
基本数据类型
关键字 内在占用 取值范围 整数类型 - byte: 1字节,-128~127 - short: 2, -32768~32767 - int: 4, -2的31次方到2的31次方-1 - long: 8, -2的63次方到2的63次方-1 浮点类型 - float: 4, 负数:-3.402823E+38到-1.401298E-45;正数: 1.401298E-45到3.402823E+38 - double: 8, 负数:-1.797693E+308至-4.9000000E-324;正数:4.9000000E-324到1.797693E+308 字符类型 - char: 2, 0-65535 布尔类型 - boolean: 1, true, false 注意 e+38表示是乘以10的38次方,同样,e-45表示乘以10的负45次方。 在java中整数默认是int类型,浮点数默认是double类型
变量
数据类型主要是用来定义变量的。
什么是变量
在程序运行过程中,其值可以在一定范围内发生改变的量,从本质上讲,变量是内存中的一小块区域,其值可以在一定范围内变化
变量定义的格式
格式一
//数据类型 变量名 = 初始化值; // 声明变量并赋值 int age = 18; long l1 = 100000000000000000000000L; System.out.println(age);
格式二
// 先声明,后赋值(使用前赋值即可) 数据类型 变量名; 变量名 = 初始化值; double money; money = 55.5; System.out.println(money);
格式三
在同一行定义多个同一种数据类型的变量,中间使用逗号隔开 //数据类型 变量名 = 值,变量名 = 值, ....; int a = 10, b = 20; // 定义int类型的变量a和b,中间使用逗号隔开 System.out.println(a); System.out.println(b); int c,d; // 声明int类型的变量c和d,中间使用逗号隔开 c = 30; d = 40; System.out.println(c); System.out.println(d);
变量注意事项
- 在同一对花括号中,变量名不能重复。
- 变量在使用之前,必须初始化(赋值)。
- 定义long类型的变量时,需要在整数的后面加L(大小写均可,建议大写)。因为整数默认是int类型,整数太大可能超出int范围。
- 定义float类型的变量时,需要在小数的后面加F(大小写均可,建议大写)。因为浮点数的默认类型是double, double的取值范围是大于float的,类型不兼容。
标识符
什么是标识符
标识符是用户编程时使用的名字,用于给类、方法、变量、常量等命名。
标识符的组成规则
由字母、数字、下划线“_”、美元符号“$”组成,第一个字符不能是数字。 不能使用java中的关键字作为标识符。 标识符对大小写敏感(区分大小写)。
标识符的命名规范(江湖规矩)
- 类名
一个单词: 首字母大写 Hello 多个单词: 每一个单词的首字母都要大写
HelloWorld (大驼峰) - 变量名和方法
一个单词: 全部小写 value get()
多个单词: 从第二个单词的首字母开始,每一个单词都要大写 maxValue getValue() (小驼峰) - 包
一个单词: 全部小写 cn com
多个单词: 全部小写中间用 . 分割 com.itfxp
公司的域名倒着写 www.baidu.com–>com.baidu.xxx - 自定义常量
一个单词: 全部大写 MAX VALUE
多个单词: 全部大写 中间用 _ 隔开 MAX_VALUE
类型转换
自动类型转换
把一个表示数据范围小的数值或者变量赋值给另一个表示数据范围大的变量。这种转换方式是自动的,直接书写即可。
示例
double num = 10; // 将int类型的10直接赋值给double类型 System.out.println(num); // 输出10.0
强制类型转换
把一个表示数据范围大的数值或者变量赋值给另一个表示数据范围小的变量。
强制类型转换格式
目标数据类型 变量名 = (目标数据类型)值或者变量;
double num1 = 5.5; int num2 = (int) num1; // 将double类型的num1强制转换为int类型 System.out.println(num2); // 输出5(小数位直接舍弃)
数据范围从小到大 - byte - short, char - int - long - float - double
强制类型转换注意事项
- char类型的数据转换为int类型是按照码表中对应的int值进行计算的。比如在ASCII码表中,'a'对应97。
int a = 'a'; System.out.println(a); // 将输出97
- 整数默认是int类型,byte、short和char类型数据参与运算均会自动转换为int类型。
byte b1 = 10; byte b2 = 20; byte b3 = b1 + b2; // 第三行代码会报错,b1和b2会自动转换为int类型,计算结果为int,int赋值给byte需要强制类型转换。 // 修改为: int num = b1 + b2; // 或者: byte b3 = (byte) (b1 + b2);
- boolean类型不能与其他基本数据类型相互转换。
运算符
什么是运算符
- 运算符
对常量或者变量进行操作的符号 - 表达式
用运算符把常量或者变量连接起来符合java语法的式子就可以称为表达式。 不同运算符连接的表达式体现的是不同类型的表达式。 举例说明
int a = 10; int b = 20; int c = a + b;
+:是运算符,并且是算术运算符。
a + b:是表达式,由于+是算术运算符,所以这个表达式叫算术表达式。
- 运算符
- 算术运算符: + - * / %
- 比较运算符:> >= < <=
= !
- 逻辑运算符:&& ||
- 赋值运算符: = += -= *= /= %=
- 自增自减运算符:++ –
- 三元运算符:表达式? 表达式1 : 表达式2
- 算术运算符: + - * / %
算术运算符
符号 作用 说明 + 加 参看小学一年级 - 减 参看小学一年级 * 乘 参看小学二年级,与“×”相同 / 除 参看小学二年级,与“÷”相同 % 取余 获取的是两个数据做除法的余数
注意事项
/和%的区别:两个数据做除法,/取结果的商,%取结果的余数。
整数操作只能得到整数,要想得到小数,必须有浮点数参与运算。
int a = 10; int b = 3; System.out.println(a / b); // 输出结果3 System.out.println(a % b); // 输出结果1
字符的"+"操作
char类型参与算术运算
使用的是计算机底层对应的十进制数值。需要我们记住三个字符对应的数值
'a' – 97 a-z是连续的,所以'b'对应的数值是98,'c'是99,依次递加
'A' – 65 A-Z是连续的,所以'B'对应的数值是66,'C'是67,依次递加
'0' – 48 0-9是连续的,所以'1'对应的数值是49,'2'是50,依次递加
// 可以通过使用字符与整数做算术运算,得出字符对应的数值是多少 char ch1 = 'a'; System.out.println(ch1 + 1); // 输出98,97 + 1 = 98 char ch2 = 'A'; System.out.println(ch2 + 1); // 输出66,65 + 1 = 66 char ch3 = '0'; System.out.println(ch3 + 1); // 输出49,48 + 1 = 49
算术表达式
算术表达式中包含不同的基本数据类型的值的时候,整个算术表达式的类型会自动进行提升。
提升规则
byte类型,short类型和char类型将被提升到int类型,不管是否有其他类型参与运算。
整个表达式的类型自动提升到与表达式中最高等级的操作数相同的类型
等级顺序:byte,short,char –> int –> long –> float –>double
演示
byte b1 = 10; byte b2 = 20; // byte b3 = b1 + b2; // 该行报错,因为byte类型参与算术运算会自动提示为int,int赋值给byte可能损失精度 int i3 = b1 + b2; // 应该使用int接收 byte b3 = (byte) (b1 + b2); // 或者将结果强制转换为byte类型 ------------------------------- int num1 = 10; double num2 = 20.0; double num3 = num1 + num2; // 使用double接收,因为num1会自动提升为double类型
正是由于上述原因,所以在程序开发中我们很少使用byte或者short类型定义整数。也很少会使用char类型定义字符,而使用字符串类型,更不会使用char类型做算术运算。
字符串的"+"操作
当“+”操作中出现字符串时,这个”+”是字符串连接符,而不是算术运算。
System.out.println("我最"+ 666); // 输出:我最666
在”+”操作中,如果出现了字符串,就是连接运算符,否则就是算术运算。当连续进行“+”操作时,从左到右逐个执行。
System.out.println(1 + 99 + "年企业"); // 输出:199年企业 System.out.println(1 + 2 + "itfxp" + 3 + 4); // 输出:3itfxp34 // 可以使用小括号改变运算的优先级 System.out.println(1 + 2 + "itfxp" + (3 + 4)); // 输出:3itfxp7
赋值运算符
赋值运算符的作用是将一个表达式的值赋给左边,左边必须是可修改的,不能是常量。
符号 作用 说明 = 赋值 a=10,将10赋值给变量a += 加后赋值 a+=b,将a+b的值给a -= 减后赋值 a-=b,将a-b的值给a *= 乘后赋值 a*=b,将a×b的值给a /= 除后赋值 a/=b,将a÷b的商给a %= 取余后赋值 a%=b,将a÷b的余数给a
注意:扩展的赋值运算符隐含了强制类型转换。
short s = 10; s = s + 10; // 此行代码报出,因为运算中s提升为int类型,运算结果int赋值给short可能损失精度 s += 10; // 此行代码没有问题,隐含了强制类型转换,相当于 s = (short) (s +10);
自增自减运算符
符号 作用 说明 ++ 自增 变量的值加1 -- 自减 变量的值减1
注意事项:
- ++和– 既可以放在变量的后边,也可以放在变量的前边。
- 单独使用的时候, ++和– 无论是放在变量的前边还是后边,结果是一样的。
- 参与操作的时候,如果放在变量的后边,先拿变量参与操作,后拿变量做++或者–。
- 参与操作的时候,如果放在变量的前边,先拿变量做++或者–,后拿变量参与操作。
- 最常见的用法:单独使用。
int i = 10; i++; // 单独使用 System.out.println("i:" + i); // i:11 int j = 10; ++j; // 单独使用 System.out.println("j:" + j); // j:11 int x = 10; int y = x++; // 赋值运算,++在后边,所以是使用x原来的值赋值给y,x本身自增1 System.out.println("x:" + x + ", y:" + y); // x:11,y:10 int m = 10; int n = ++m; // 赋值运算,++在前边,所以是使用m自增后的值赋值给n,m本身自增1 System.out.println("m:" + m + ", m:" + m); // m:11,m:11
练习:
int x = 10; int y = x++ + x++ + x++; System.out.println(y); // y的值是多少? /* 解析,三个表达式都是++在后,所以每次使用的都是自增前的值,但程序自左至右执行,所 以第一次自增时,使用的是10进行计算,但第二次自增时,x的值已经自增到11了,所以第 二次使用的是11,然后再次自增。。。 所以整个式子应该是:int y = 10 + 11 + 12; 输出结果为33。 */ 注意:通过此练习深刻理解自增和自减的规律,但实际开发中强烈建议不要写这样的代码!小心挨打!
关系运算符
关系运算符有6种关系,分别为小于、小于等于、大于、等于、大于等于、不等于。
符号 说明
== a==b,判断a和b的值是否相等,成立为true,不成立为false
!= a!=b,判断a和b的值是否不相等,成立为true,不成立为false
> a>b,判断a是否大于b,成立为true,不成立为false
>= a>=b,判断a是否大于等于b,成立为true,不成立为false
< a<b,判断a是否小于b,成立为true,不成立为false
<= a<=b,判断a是否小于等于b,成立为true,不成立为false
注意事项
关系运算符的结果都是boolean类型,要么是true,要么是false。 千万不要把 “==”误写成“=”,"==" 是判断是否相等的关系,"="是赋值。
int a = 10; int b = 20; System.out.println(a == b); // false System.out.println(a != b); // true System.out.println(a > b); // false System.out.println(a >= b); // false System.out.println(a < b); // true System.out.println(a <= b); // true // 关系运算的结果肯定是boolean类型,所以也可以将运算结果赋值给boolean类型的变量 // 比较运算符经常结合 if 判断使用 boolean flag = a > b; System.out.println(flag); // 输出false
逻辑运算符
逻辑运算符把各个运算的关系表达式连接起来组成一个复杂的逻辑表达式,以判断程序中的表达式是否成立,判断的结果是 true 或 false。
符号 作用 说明 & 逻辑与 a&b,a和b都是true,结果为true,否则为false | 逻辑或 a|b,a和b都是false,结果为false,否则为true ^ 逻辑异或 a^b,a和b结果不同为true,相同为false ! 逻辑非 !a,结果和a的结果正好相反
代码演示
//定义变量 int i = 10; int j = 20; int k = 30; //& “与”,并且的关系,只要表达式中有一个值为false,结果即为false System.out.println((i > j) & (i > k)); //false & false,输出false System.out.println((i < j) & (i > k)); //true & false,输出false System.out.println((i > j) & (i < k)); //false & true,输出false System.out.println((i < j) & (i < k)); //true & true,输出true System.out.println("--------"); //| “或”,或者的关系,只要表达式中有一个值为true,结果即为true System.out.println((i > j) | (i > k)); //false | false,输出false System.out.println((i < j) | (i > k)); //true | false,输出true System.out.println((i > j) | (i < k)); //false | true,输出true System.out.println((i < j) | (i < k)); //true | true,输出true System.out.println("--------"); //^ “异或”,相同为false,不同为true System.out.println((i > j) ^ (i > k)); //false ^ false,输出false System.out.println((i < j) ^ (i > k)); //true ^ false,输出true System.out.println((i > j) ^ (i < k)); //false ^ true,输出true System.out.println((i < j) ^ (i < k)); //true ^ true,输出false System.out.println("--------"); //! “非”,取反 System.out.println((i > j)); //false System.out.println(!(i > j)); //!false,,输出true
短路逻辑运算符
符号 作用 说明 && 短路与 作用和&相同,但是有短路效果 || 短路或 作用和|相同,但是有短路效果
在逻辑与运算中,只要有一个表达式的值为false,那么结果就可以判定为false了,没有必要将所有表达式的值都计算出来,短路与操作就有这样的效果,可以提高效率。同理在逻辑或运算中,一旦发现值为true,右边的表达式将不再参与运算。
- 逻辑与&,无论左边真假,右边都要执行。
- 短路与&&,如果左边为真,右边执行;如果左边为假,右边不执行。
- 逻辑或|,无论左边真假,右边都要执行。
- 短路或||,如果左边为假,右边执行;如果左边为真,右边不执行。
int x = 3; int y = 4; System.out.println((x++ > 4) & (y++ > 5)); // 两个表达都会运算 System.out.println(x); // 4 System.out.println(y); // 5 System.out.println((x++ > 4) && (y++ > 5)); // 左边已经可以确定结果为false,右边不参与运算 System.out.println(x); // 4 System.out.println(y); // 4
三元运算符
三元运算符语法格式
关系表达式 ? 表达式1 : 表达式2;
格式解析
问号前面的位置是判断的条件,判断结果为boolean型,为true时调用表达式1,为false时调用表达式2。其逻辑为:如果条件表达式成立或者满足则执行表达式1,否则执行第二个。
举例
int a = 10; int b = 20; int c = a > b ? a : b; // 判断 a>b 是否为真,如果为真取a的值,如果为假,取b的值
三元运算符案例
需求1
动物园里有两只老虎,已知两只老虎的体重分别为180kg、200kg,请用程序实现判断两只老虎的体重是否相同。
public class OperatorTest01 { public static void main(String[] args) { //1:定义两个变量用于保存老虎的体重,单位为kg,这里仅仅体现数值即可。 int weight1 = 180; int weight2 = 200; //2:用三元运算符实现老虎体重的判断,体重相同,返回true,否则,返回false。 boolean b = weight1 == weight2 ? true : false; //3:输出结果 System.out.println("b:" + b); } }
需求2
一座寺庙里住着三个和尚,已知他们的身高分别为150cm、210cm、165cm,请用程序实现获取这三个和尚的最高身高。
public class OperatorTest02 { public static void main(String[] args) { //1:定义三个变量用于保存和尚的身高,单位为cm,这里仅仅体现数值即可。 int height1 = 150; int height2 = 210; int height3 = 165; //2:用三元运算符获取前两个和尚的较高身高值,并用临时身高变量保存起来。 int tempHeight = height1 > height2 ? height1 : height2; //3:用三元运算符获取临时身高值和第三个和尚身高较高值,并用最大身高变量保存。 int maxHeight = tempHeight > height3 ? tempHeight : height3; //4:输出结果 System.out.println("maxHeight:" + maxHeight); } }
数据输入
我们可以通过 Scanner 类来获取用户的输入。使数据达到灵活性
使用步骤
导包
Scanner 类在java.util包下,所以需要将该类导入。导包的语句需要定义在类的上面。
import java.util.Scanner;
创建Scanner对象
Scanner sc = new Scanner(System.in);// 创建Scanner对象,sc表示变量名,其他均不可变
接收数据
int i = sc.nextInt(); // 表示将键盘录入的值作为int数返回。
示例:
import java.util.Scanner; public class ScannerDemo { public static void main(String[] args) { //创建对象 Scanner sc = new Scanner(System.in); //接收数据 int x = sc.nextInt(); //输出数据 System.out.println("x:" + x); } }
案例
改写三个和尚案例,数据使用键盘录入。
import java.util.Scanner; public class ScannerTest { public static void main(String[] args) { //身高未知,采用键盘录入实现。首先导包,然后创建对象。 Scanner sc = new Scanner(System.in); //键盘录入三个身高分别赋值给三个变量。 System.out.println("请输入第一个和尚的身高:"); int height1 = sc.nextInt(); System.out.println("请输入第二个和尚的身高:"); int height2 = sc.nextInt(); System.out.println("请输入第三个和尚的身高:"); int height3 = sc.nextInt(); //用三元运算符获取前两个和尚的较高身高值,并用临时身高变量保存起来。 int tempHeight = height1 > height2 ? height1 : height2; //用三元运算符获取临时身高值和第三个和尚身高较高值,并用最大身高变量保存。 int maxHeight = tempHeight > height3 ? tempHeight : height3; //输出结果。 System.out.println("这三个和尚中身高最高的是:" + maxHeight +"cm"); } }
流程控制
在一个程序执行的过程中,各条语句的执行顺序对程序的结果是有直接影响的。所以,我们必须清楚每条语句的执行流程。而且,很多时候要通过控制语句的执行顺序来实现我们想要的功能。
流程控制语句分类
顺序结构
分支结构(if, switch)
循环结构(for, while, do…while)
顺序结构
顺序结构是程序中最简单最基本的流程控制,没有特定的语法结构,按照代码的先后顺序,依次执行,程序中大多数的代码都是这样执行的。
顺序结构执行流程图
开始–语句1–语句2–语句3–结束
分支结构之if语句
- if格式一
if (关系表达式) {
语句体;
}
执行流程
①首先计算关系表达式的值
②如果关系表达式的值为true就执行语句体
③如果关系表达式的值为false就不执行语句体
④继续执行后面的语句内容
代码示例
public class IfDemo { public static void main(String[] args) { System.out.println("开始"); //定义两个变量 int a = 10; int b = 20; //需求:判断a和b的值是否相等,如果相等,就在控制台输出:a等于b if(a == b) { System.out.println("a等于b"); } //需求:判断a和c的值是否相等,如果相等,就在控制台输出:a等于c int c = 10; if(a == c) { System.out.println("a等于c"); } System.out.println("结束"); } }
- if格式二
if (关系表达式) { 语句体1; } else { 语句体2; }
执行流程
①首先计算关系表达式的值
②如果关系表达式的值为true就执行语句体1
③如果关系表达式的值为false就执行语句体2
④继续执行后面的语句内容
代码示例
public class IfDemo02 { public static void main(String[] args) { System.out.println("开始"); //定义两个变量 int a = 10; int b = 20; b = 5; //需求:判断a是否大于b,如果是,在控制台输出:a的值大于b,否则,在控制台输出:a的值不大于b if(a > b) { System.out.println("a的值大于b"); } else { System.out.println("a的值不大于b"); } System.out.println("结束"); } }
if语句案例:奇偶数
需求
任意给出一个整数,请用程序实现判断该整数是奇数还是偶数,并在控制台输出该整数是奇数还是偶数。
分析
①为了体现任意给出一个整数,采用键盘录入一个数据
②判断整数是偶数还是奇数要分两种情况进行判断,使用if..else结构
③判断是否偶数需要使用取余运算符实现该功能 number % 2 == 0
④根据判定情况,在控制台输出对应的内容
代码实现
import java.util.Scanner; public class IfTest01 { public static void main(String[] args) { //为了体现任意给出一个整数,采用键盘录入一个数据。(导包,创建对象,接收数据) Scanner sc = new Scanner(System.in); System.out.println("请输入一个整数:"); int number = sc.nextInt(); //判断整数是偶数还是奇数要分两种情况进行判断,使用if..else结构 //判断是否偶数需要使用取余运算符实现该功能 number % 2 == 0 //根据判定情况,在控制台输出对应的内容 if(number%2 == 0) { System.out.println(number + "是偶数"); } else { System.out.println(number + "是奇数"); } } }
- if格式三
if (关系表达式1) { 语句体1; } else if (关系表达式2) { 语句体2; } … else { 语句体n+1; }
执行流程
①首先计算关系表达式1的值
②如果值为true就执行语句体1;如果值为false就计算关系表达式2的值
③如果值为true就执行语句体2;如果值为false就计算关系表达式3的值
④…
⑤如果没有任何关系表达式为true,就执行语句体n+1。
代码示例
键盘录入一个星期数(1,2,…7),输出对应的星期一,星期二,…星期日
import java.util.Scanner; public class IfDemo03 { public static void main(String[] args) { System.out.println("开始"); // 需求:键盘录入一个星期数(1,2,...7),输出对应的星期一,星期二,...星期日 Scanner sc = new Scanner(System.in); System.out.println("请输入一个星期数(1-7):"); int week = sc.nextInt(); if(week == 1) { System.out.println("星期一"); } else if(week == 2) { System.out.println("星期二"); } else if(week == 3) { System.out.println("星期三"); } else if(week == 4) { System.out.println("星期四"); } else if(week == 5) { System.out.println("星期五"); } else if(week == 6) { System.out.println("星期六"); } else { System.out.println("星期日"); } System.out.println("结束"); } }
if语句格式3案例
案例需求
小明快要期末考试了,小明爸爸对他说,会根据他不同的考试成绩,送他不同的礼物,假如你可以控制小明的得分,请用程序实现小明到底该获得什么样的礼物,并在控制台输出。
案例分析
①小明的考试成绩未知,可以使用键盘录入的方式获取值
②由于奖励种类较多,属于多种判断,采用if…else…if格式实现
③为每种判断设置对应的条件
④为每种判断设置对应的奖励
案例代码
import java.util.Scanner; public class IfTest02 { public static void main(String[] args) { //小明的考试成绩未知,可以使用键盘录入的方式获取值 Scanner sc = new Scanner(System.in); System.out.println("请输入一个分数:"); int score = sc.nextInt(); //由于奖励种类较多,属于多种判断,采用if...else...if格式实现 //为每种判断设置对应的条件 //为每种判断设置对应的奖励 //数据测试:正确数据,边界数据,错误数据 if(score>100 || score<0) { System.out.println("你输入的分数有误"); } else if(score>=95 && score<=100) { System.out.println("山地自行车一辆"); } else if(score>=90 && score<=94) { System.out.println("游乐场玩一次"); } else if(score>=80 && score<=89) { System.out.println("变形金刚玩具一个"); } else { System.out.println("胖揍一顿"); } } }
switch语句
switch语句格式
switch (表达式) { case 值1: 语句体1; break; case 值2: 语句体2; break; ... default: 语句体n+1; break; }
执行流程
- 首先计算出表达式的值
- 其次,和case依次比较,一旦有对应的值,就会执行相应的语句,在执行的过程中,遇到break就会结 束。
- 最后,如果所有的case都和表达式的值不匹配,就会执行default语句体部分,然后程序结束掉。
switch语句案例-春夏秋冬
需求
一年有12个月,分属于春夏秋冬4个季节,键盘录入一个月份,请用程序实现判断该月份属于哪个季节,并输出。
代码实现
public class Demo1 { public static void main(String[] args) { //键盘录入月份数据,使用变量接收 Scanner sc = new Scanner(System.in); System.out.println("请输入一个月份:"); int month = sc.nextInt(); //case穿透 switch(month) { case 1: case 2: case 12: System.out.println("冬季"); break; case 3: case 4: case 5: System.out.println("春季"); break; case 6: case 7: case 8: System.out.println("夏季"); break; case 9: case 10: case 11: System.out.println("秋季"); break; default: System.out.println("你输入的月份有误"); } } }
运行结果
春:3、4、5
夏:6、7、8
秋:9、10、11
冬:1、2、12
注意:如果switch中得case,没有对应break的话,则会出现case穿透的现象。
for循环
循环语句可以在满足循环条件的情况下,反复执行某一段代码,这段被重复执行的代码被称为循环体语句,当反复 执行这个循环体时,需要在合适的时候把循环判断条件修改为false,从而结束循环,否则循环将一直执行下去,形 成死循环。
for循环结构
for (初始化语句;条件判断语句;条件控制语句) {
循环体语句;
}
格式解释:
- 初始化语句: 用于表示循环开启时的起始状态,简单说就是循环开始的时候什么样
- 条件判断语句:用于表示循环反复执行的条件,简单说就是判断循环是否能一直执行下去
- 循环体语句: 用于表示循环反复执行的内容,简单说就是循环反复执行的事情
- 条件控制语句:用于表示循环执行中每次变化的内容,简单说就是控制循环是否能执行下去
执行流程
①执行初始化语句 ②执行条件判断语句,看其结果是true还是false 如果是false,循环结束 如果是true,继续执行 ③执行循环体语句 ④执行条件控制语句 ⑤回到②继续
for循环案例
案例一:输出数据
需求
在控制台输出1-5和5-1的数据
代码实现
public class ForTest01 { public static void main(String[] args) { //需求:输出数据1-5 for(int i=1; i<=5; i++) { System.out.println(i); } System.out.println("--------"); //需求:输出数据5-1 for(int i=5; i>=1; i--) { System.out.println(i); } } }
案例二:求和
需求
求1-5之间的数据和,并把求和结果在控制台输出
代码实现
public class ForTest02 { public static void main(String[] args) { //求和的最终结果必须保存起来,需要定义一个变量,用于保存求和的结果,初始值为0 int sum = 0; //从1开始到5结束的数据,使用循环结构完成 for(int i=1; i<=5; i++) { //将反复进行的事情写入循环结构内部 // 此处反复进行的事情是将数据 i 加到用于保存最终求和的变量sum 中 sum += i; /* sum += i; sum = sum + i; 第一次:sum = sum + i = 0 + 1 = 1; 第二次:sum = sum + i = 1 + 2 = 3; 第三次:sum = sum + i = 3 + 3 = 6; 第四次:sum = sum + i = 6 + 4 = 10; 第五次:sum = sum + i = 10 + 5 = 15; */ } //当循环执行完毕时,将最终数据打印出来 System.out.println("1-5之间的数据和是:" + sum); } }
本题要点
- 今后遇到的需求中,如果带有求和二字,请立即联想到求和变量
- 求和变量的定义位置,必须在循环外部,如果在循环内部则计算出的数据将是错误的
案例三:偶数和
需求
求1-100之间的偶数和,并把求和结果在控制台输出
代码实现
public class ForTest03 { public static void main(String[] args) { //求和的最终结果必须保存起来,需要定义一个变量,用于保存求和的结果,初始值为0 int sum = 0; //对1-100的数据求和与1-5的数据求和几乎完全一样,仅仅是结束条件不同 for(int i=1; i<=100; i++) { //对1-100的偶数求和,需要对求和操作添加限制条件,判断是否是偶数 if(i%2 == 0) { sum += i; } } //当循环执行完毕时,将最终数据打印出来 System.out.println("1-100之间的偶数和是:" + sum); } }
案例四:水仙花
需求
在控制台输出所有的“水仙花数”
解释:什么是水仙花数
水仙花数,指的是一个三位数,个位、十位、百位的数字立方和等于原数
例如 153 3*3*3 + 5*5*5 + 1*1*1 = 153
思路分析
- 获取所有的三位数,准备进行筛选,最小的三位数为100,最大的三位数为999,使用for循环获取
- 获取每一个三位数的个位,十位,百位,做if语句判断是否是水仙花数
代码实现
public class ForTest04 { public static void main(String[] args) { //输出所有的水仙花数必然要使用到循环,遍历所有的三位数,三位数从100开始,到999结束 for(int i=100; i<1000; i++) { //在计算之前获取三位数中每个位上的值 int ge = i%10; int shi = i/10%10; int bai = i/10/10%10; //判定条件是将三位数中的每个数值取出来,计算立方比较是否相等 if(ge*ge*ge + shi*shi*shi + bai*bai*bai == i) { //输出满足条件的数字就是水仙花数 System.out.println(i); } } } }
案例五:统计水仙花个数
需求
统计“水仙花数”一共有多少个,并在控制台输出个数
代码实现
public class ForTest05 { public static void main(String[] args) { //定义变量count,用于保存“水仙花数”的数量,初始值为0 int count = 0; //输出所有的水仙花数必然要使用到循环,遍历所有的三位数,三位数从100开始,到999结束 for(int i=100; i<1000; i++) { //在计算之前获取三位数中每个位上的值 int ge = i%10; int shi = i/10%10; int bai = i/10/10%10; //在判定水仙花数的过程中,满足条件不再输出,更改为修改count的值,使count+1 if(ge*ge*ge + shi*shi*shi + bai*bai*bai == i) { count++; } } //打印输出最终结果 System.out.println("水仙花共有:" + count + "个"); } }
本题要点
今后如果需求带有统计xxx,请先想到计数器变量
计数器变量定义的位置,必须在循环外部
while循环
while结构
初始化语句;
while (条件判断语句) {
循环体语句;
条件控制语句;
}
while循环执行流程
①执行初始化语句 ②执行条件判断语句,看其结果是true还是false 如果是false,循环结束 如果是true,继续执行 ③执行循环体语句 ④执行条件控制语句 ⑤回到②继续
案例代码
public class WhileDemo { public static void main(String[] args) { //需求:在控制台输出5次"HelloWorld" //for循环实现 for(int i=1; i<=5; i++) { System.out.println("HelloWorld"); } System.out.println("--------"); //while循环实现 int j = 1; while(j<=5) { System.out.println("HelloWorld"); j++; } } }
while循环案例
需求
世界最高山峰是珠穆朗玛峰(8844.43米=8844430毫米),假如我有一张足够大的纸,它的厚度是0.1毫米。请问,我折叠多少次,可以折成珠穆朗玛峰的高度?
代码实现
public class WhileTest { public static void main(String[] args) { //定义一个计数器,初始值为0 int count = 0; //定义纸张厚度 double paper = 0.1; //定义珠穆朗玛峰的高度 int zf = 8844430; //因为要反复折叠,所以要使用循环,但是不知道折叠多少次,这种情况下更适合使用while循环 //折叠的过程中当纸张厚度大于珠峰就停止了,因此继续执行的要求是纸张厚度小于珠峰高度 while(paper <= zf) { //循环的执行过程中每次纸张折叠,纸张的厚度要加倍 paper *= 2; //在循环中执行累加,对应折叠了多少次 count++; } //打印计数器的值 System.out.println("需要折叠:" + count + "次"); } }
do…while循环
do…while循环格式
初始化语句; do { 循环体语句; 条件控制语句; }while(条件判断语句);
执行流程
① 执行初始化语句 ② 执行循环体语句 ③ 执行条件控制语句 ④ 执行条件判断语句,看其结果是true还是false 如果是false,循环结束 如果是true,继续执行 ⑤ 回到②继续
代码演示
public class DoWhileDemo { public static void main(String[] args) { //需求:在控制台输出5次"HelloWorld" //for循环实现 for(int i=1; i<=5; i++) { System.out.println("HelloWorld"); } System.out.println("--------"); //do...while循环实现 int j = 1; do { System.out.println("HelloWorld"); j++; }while(j<=5); } }
三种循环的区别
for、while和do…while
- for循环和while循环先判断条件是否成立,然后决定是否执行循环体(先判断后执行)
- do…while循环先执行一次循环体,然后判断条件是否成立,是否继续执行循环体(先执行后判断)
for循环和while的区别
- 条件控制语句所控制的自增变量,因为归属for循环的语法结构中,在for循环结束后,就不能再次被访问到了
- 条件控制语句所控制的自增变量,对于while循环来说不归属其语法结构中,在while循环结束后,该变量还可以继续使用
死循环的三种格式
- for(;;){}
- while(true){}
- do {} while(true)
跳转控制语句
跳转控制语句(break)
跳出循环,结束循环
跳转控制语句(continue)
跳过本次循环,继续下次循环
注意事项
break和continue只能在循环中进行使用,单独使用无任何意义!!
循环嵌套
循环嵌套概述
在循环中,继续定义循环
示例代码
public static void main(String[] args) { //外循环控制小时的范围,内循环控制分钟的范围 for (int hour = 0; hour < 24; hour++) { for (int minute = 0; minute < 60; minute++) { System.out.println(hour + "时" + minute + "分"); } System.out.println("--------"); } }
结论
外循环执行一次,内循环执行一圈
嵌套循环案例
需求
使用嵌套for循环打印九九乘法表
思路分析
1. 定义外层循环控制有多少行 for(int i = 1 ; i <= 9 i ++) 2. 定义内层循环控制每一行输出多少个数据 for(int j = 1 ; j <= i ; j++) 3. 拼接输出 1 * 1 = 1 1 * 2 = 2 2 * 2 = 4
代码实现
public class TestJJ{ public static void main(String[] args){ for(int i = 1 ; i <= 9;i ++){ for(int j = 1 ; j <= i ; j++){ System.out.print(j + " * " + i + " = " + (i*j) +"\t"); } System.out.println(); } } }
Random
Random概述
Random类似Scanner,也是Java提供好的API,内部提供了产生随机数的功能
API后续课程详细讲解,现在可以简单理解为Java已经写好的代码
使用步骤
导入包 import java.util.Random; 创建对象 Random r = new Random(); 产生随机数 int num = r.nextInt(10);
解释: 10代表的是一个范围,如果括号写10,产生的随机数就是0-9,括号写20,参数的随机数则是0-19
示例代码
import java.util.Random; public class RandomDemo { public static void main(String[] args) { //创建对象 Random r = new Random(); //用循环获取10个随机数 for(int i=0; i<10; i++) { //获取随机数 int number = r.nextInt(10); System.out.println("number:" + number); } //需求:获取一个1-100之间的随机数 int x = r.nextInt(100) + 1; System.out.println(x); } }
Random练习-猜数字
需求
程序自动生成一个1-100之间的数字,使用程序实现猜出这个数字是多少? 当猜错的时候根据不同情况给出相应的提示 如果猜的数字比真实数字大,提示你猜的数据大了 如果猜的数字比真实数字小,提示你猜的数据小了 如果猜的数字与真实数字相等,提示恭喜你猜中了
示例代码
import java.util.Random; import java.util.Scanner; public class RandomTest { public static void main(String[] args) { //要完成猜数字的游戏,首先需要有一个要猜的数字,使用随机数生成该数字,范围1到100 Random r = new Random(); int number = r.nextInt(100) + 1; while(true) { //使用程序实现猜数字,每次均要输入猜测的数字值,需要使用键盘录入实现 Scanner sc = new Scanner(System.in); System.out.println("请输入你要猜的数字:"); int guessNumber = sc.nextInt(); //比较输入的数字和系统产生的数据,需要使用分支语句。 //这里使用if..else..if..格式,根据不同情况进行猜测结果显示 if(guessNumber > number) { System.out.println("你猜的数字" + guessNumber + "大了"); } else if(guessNumber < number) { System.out.println("你猜的数字" + guessNumber + "小了"); } else { System.out.println("恭喜你猜中了"); break; } } } }
一维数组
什么是数组
数组就是存储数据长度固定的容器,存储多个数据的数据类型要一致。
数组定义格式
格式一
数据类型[] 数组名;
示例:
int[] arr; double[] arr; char[] arr;
格式二
数据类型 数组名[];
示例:
int arr[]; double arr[]; char arr[];
数组初始化
数组动态初始化
什么是动态初始化
数组动态初始化就是只给定数组的长度,由系统给出默认初始化值
动态初始化格式
数据类型[] 数组名 = new 数据类型[数组长度];
int[] arr = new int[3];
动态初始化格式详解
- 等号左边
- int:数组的数据类型
- []:代表这是一个数组
- arr:代表数组的名称
- int:数组的数据类型
- 等号右边
- new:为数组开辟内存空间
- int:数组的数据类型
- []:代表这是一个数组
- 3:代表数组的长度
- new:为数组开辟内存空间
数组静态初始化
什么是静态初始化
在创建数组时,直接将元素确定,由系统计算出数组的长度
静态初始化格式
- 完整版格式
数据类型[] 数组名 = new 数据类型[]{元素1,元素2,...};
- 简化版格式
数据类型[] 数组名 = {元素1,元素2,...};
示例代码
public class ArrayDemo { public static void main(String[] args) { //定义数组 int[] arr = {1, 2, 3}; //输出数组名 System.out.println(arr); //输出数组中的元素 System.out.println(arr[0]); System.out.println(arr[1]); System.out.println(arr[2]); } }
数组元素访问
什么是索引
每一个存储到数组的元素,都会自动的拥有一个编号,从0开始。
这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素。
访问数组元素格式
数组名[索引];
示例代码
public class ArrayDemo { public static void main(String[] args) { int[] arr = new int[3]; //输出数组名 System.out.println(arr); //[I@880ec60 //输出数组中的元素 System.out.println(arr[0]); System.out.println(arr[1]); System.out.println(arr[2]); } }
数组一些操作
int[] arr = new int[3]; //问题1: 怎么向数组中存储元素 // 语法: 数组名[下标] = 值 arr[0] = 90; arr[1] = 70; arr[2] = 60; // arr[3] = 60; // 报错,数组下标越界,不能等于大于数组的长度 //问题2: 怎么从数组中获取元素 //语法: 数组名[下标] System.out.println(arr[0]); int t = arr[0]; //问题3: 怎么计算数组的长度 //语法: arr.length System.out.println(arr.length);
内存分配
内存概述
内存是计算机中的重要原件,临时存储区域,作用是运行程序。
我们编写的程序是存放在硬盘中的,在硬盘中的程序是不会运行的。
必须放进内存中才能运行,运行完毕后会清空内存。
Java虚拟机要运行程序,必须要对内存进行空间的分配和管理。
java中的内存分配
目前我们只需要记住两个内存,分别是:栈内存和堆内存
区域名称 作用 寄存器 给CPU使用,和我们开发无关。 本地方法栈 JVM在使用操作系统功能的时候使用,和我们开发无关。 方法区 存储可以运行的class文件。 堆内存 存储对象或者数组,new来创建的,都存储在堆内存。 方法栈 方法运行时使用的内存,比如main方法运行,进入方法栈中执行。
数组内存图
单个数组的内存图
当我们
多个数组的内存图
多个数组指向相同内存图
数组操作的两个常见小问题
索引越界异常
- 出现原因
public class ArrayDemo { public static void main(String[] args) { int[] arr = new int[3]; System.out.println(arr[3]); } }
数组长度为3,索引范围是0~2,但是我们却访问了一个3的索引。
程序运行后,将会抛出ArrayIndexOutOfBoundsException 数组越界异常。在开发中,数组的越界异常是不能出现的,一旦出现了,就必须要修改我们编写的代码。
解决方案
将错误的索引修改为正确的索引范围即可!
空指针异常
- 出现原因
public class ArrayDemo { public static void main(String[] args) { int[] arr = new int[3]; //把null赋值给数组 arr = null; System.out.println(arr[0]); } }
arr = null 这行代码,意味着变量arr将不会在保存数组的内存地址,也就不允许再操作数组了,因此运行的时候会抛出 NullPointerException空指针异常。在开发中,数组的越界异常是不能出现的,一旦出现了,就必须要修改我们编写的代码。
解决方案
给数组一个真正的堆内存空间引用即可!
数组案例
数组遍历
就是将数组中的每个元素分别获取出来,就是遍历。遍历也是数组操作中的基石。
public class ArrayTest01 { public static void main(String[] args) { int[] arr = { 1, 2, 3, 4, 5 }; System.out.println(arr[0]); System.out.println(arr[1]); System.out.println(arr[2]); System.out.println(arr[3]); System.out.println(arr[4]); } }
以上代码是可以将数组中每个元素全部遍历出来,但是如果数组元素非常多,这种写法肯定不行,因此我们需要改造成循环的写法。数组的索引是 0 到 lenght-1 ,可以作为循环的条件出现。
public class ArrayTest01 { public static void main(String[] args) { //定义数组 int[] arr = {11, 22, 33, 44, 55}; //使用通用的遍历格式 for(int x=0; x<arr.length; x++) { System.out.println(arr[x]); } } }
public class ArrayTest01 { public static void main(String[] args) { //定义数组 int[] arr = {11, 22, 33, 44, 55}; //使用通用的遍历格式 for(int i : arr ) { System.out.println(i); } } }
数组最值
数组最值就是从数组的所有元素中找出最大值或最小值
实现思路
- 定义变量,保存数组0索引上的元素
- 遍历数组,获取出数组中的每个元素
- 将遍历到的元素和保存数组0索引上值的变量进行比较
- 如果数组元素的值大于了变量的值,变量记录住新的值
- 数组循环遍历结束,变量保存的就是数组中的最大值
代码实现
public class ArrayTest02 { public static void main(String[] args) { //定义数组 int[] arr = {12, 45, 98, 73, 60}; //定义一个变量,用于保存最大值 //取数组中第一个数据作为变量的初始值 int max = arr[0]; //与数组中剩余的数据逐个比对,每次比对将最大值保存到变量中 for(int x=1; x<arr.length; x++) { if(arr[x] > max) { max = arr[x]; } } //循环结束后打印变量的值 System.out.println("max:" + max); } }
数组元素反转
数组中的元素颠倒顺序,例如原始数组为1,2,3,4,5,反转后的数组为5,4,3,2,1
实现思路
数组最远端的元素互换位置。 实现反转,就需要将数组最远端元素位置交换 定义两个变量,保存数组的最小索引和最大索引 两个索引上的元素交换位置 最小索引++,最大索引--,再次交换位置 最小索引超过了最大索引,数组反转操作结束
代码实现
public static void main(String[] args) { int[] arr = { 1, 2, 3, 4, 5 }; /* 循环中定义变量min=0最小索引 max=arr.length‐1最大索引 min++,max‐‐ */ for (int min = 0, max = arr.length - 1; min <= max; min++, max--) { //利用第三方变量完成数组中的元素交换 int temp = arr[min]; arr[min] = arr[max]; arr[max] = temp; } // 反转后,遍历数组 for (int i = 0; i < arr.length; i++) { System.out.println(arr[i]); } }
函数
方法概述
什么是方法
方法(method)是将具有独立功能的代码块组织成为一个整体,使其具有特殊功能的代码集
方法基本使用
将资料中给大家提供的打怪物发射炮弹重复的代码,抽取到方法中并使用
抽取代码
public static void fire() { System.out.println("准备发射5发炮弹"); System.out.println("发射第1发炮弹* * * *"); System.out.println("发射第2发炮弹* * * *"); System.out.println("发射第3发炮弹* * * *"); System.out.println("发射第4发炮弹* * * *"); System.out.println("发射第5发炮弹* * * *"); System.out.println("发射5发炮弹结束"); }
在main方法中使用
public static void main(String[] args) { System.out.println("游戏开始..."); System.out.println("看到了一个怪物...血牙野猪..."); //调用方法,发射炮弹 fire(); System.out.println("...血牙野猪被打倒..."); }
调用格式
方法名();
注意
- 方法必须先创建才可以使用,该过程成为方法定义
- 方法创建后并不是直接可以运行的,需要手动使用后,才执行,该过程成为方法调用
定义方法格式
修饰符 返回值类型 方法名(参数列表){ //代码省略... return 结果; }
- 修饰符 : public static固定写法
- 返回值类型 : 表示方法运行的结果的数据类型,方法执行后将结果返回到调用者
- 参数列表 : 方法在运算过程中的未知数据,调用者调用方法时传递
- return : 将方法执行后的结果带给调用者,方法执行到 return ,整体方法运行结束
小贴士:return 结果; 这里的"结果"在开发中,我们正确的叫法成为方法的返回值
方法案例
定义方法的两个明确
- 明确参数列表
该方法在完成一个功能时,需要的参数有几个,参数的类型是什么,需要在我们明确给出的。 - 明确返回值类型
方法的功能完成之后,是否有结果返回,如果有,使用return 将结果返回给调用者。
案例一
需求
在控制台打印10次HelloWorld
分析
- 明确参数列表
方法中打印出 HelloWorld 即可,没有计算结果,返回值类型 void - 明确返回值
打印几次已经明确规定10次,参数不需要定义
代码实现
public class MethodDemo01 { public static void main(String[] args) { //调用方法printHelloWorld打印10次HelloWorld printHelloWorld(); } public static void printHelloWorld() { for (int i = 0; i < 10; i++) { System.out.println("HelloWorld"); } } }
案例二
需求
实现不定次数打印HelloWorld
分析
- 明确参数列表
打印几次不清楚,参数定义一个整型参数 - 明确返回值
方法中打印出 HelloWorld 即可,没有计算结果,返回值类型 void
代码实现
public class MethodDemo02 { public static void main(String[] args) { //调用方法printHelloWorld,传递整数 printHelloWorld(); } /* 定义打印HelloWorld方法 返回值类型,计算没有结果 void 参数:不确定打印几次 */ public static void printHelloWorld(int n) { for (int i = 0; i < n; i++) { System.out.println("HelloWorld"); } } }
案例三
需求
定义方法实现两个整数的求和计算
分析
- 明确参数列表
计算哪两个整数的和,并不清楚,但可以确定是整数,参数列表可以定义两个int类型的变量,由调用者调用方法时传递 - 明确返回值
方法计算的是整数的求和,结果也必然是个整数,返回值类型定义为int类型。
代码实现
public class MethodDemo03 { public static void main(String[] args) { // 调用方法getSum,传递两个整数,这里传递的实际数据又称为实际参数 // 并接收方法计算后的结果,返回值 int sum = getSum(5, 6); System.out.println(sum); } /* 定义计算两个整数和的方法 返回值类型,计算结果是int 参数:不确定数据求和,定义int参数.参数又称为形式参数 */ public static int getSum(int a, int b) { return a + b; } }
案例四
需求
定义方法实现比较两个整数是否相同
分析
- 明确参数列表
比较的两个整数不确定,所以默认定义两个int类型的参数。 - 明确返回值
比较整数,比较的结果只有两种可能,相同或不同,因此结果是布尔类型,比较的结果相同为true。
代码实现
public class MethodDemo04 { public static void main(String[] args) { //调用方法compare,传递两个整数 //并接收方法计算后的结果,布尔值 boolean bool = compare(3, 8); System.out.println(bool); } /* 定义比较两个整数是否相同的方法 返回值类型,比较的结果布尔类型 参数:不确定参与比较的两个整数 */ public static boolean compare(int a, int b) { if (a == b) { return true; } else { return false; } } }
案例五
需求
定义方法实现:计算1+2+3…+100的和
分析
- 明确参数列表
需求中已知到计算的数据,没有未知的数据,不定义参数 - 明确返回值
1~100的求和,计算后必然还是整数,返回值类型是int
代码实现
public class MethodDemo05 { public static void main(String[] args) { //调用方法getSum //并接收方法计算后的结果,整数 int sum = getSum(); System.out.println(sum); } /* 定义计算1~100的求和方法 返回值类型,计算结果整数int 参数:没有不确定数据 */ public static int getSum() { //定义变量保存求和 int sum = 0; //从1开始循环,到100结束 for (int i = 1; i <= 100; i++) { sum = sum + i; } return sum; } }
方法小结
定义方法注意事项
- 定义位置:类中方法外,不能嵌套定义
- 方法必须先定义,再使用
- void表示无返回值,可以省略return,也可以单独的书写return
- 不能在return后面写代码,return意味着方法结束,所有后面的代码记永远都不会执行,属于无效代码
- 定义方法时()中的参数,我们称之为:形式参数,在调用该方法时传递的参数,我们称之为:实际参数
java中方法中局部变量是没有默认值的。
public static void kk() { int i; // 局部变量没有默认值 System.out.println(i); }
java中形参是不能给默认值的,python可以
调用方法三种形式
直接调用 :直接写方法名调用
public static void main(String[] args) { print(); } public static void print() { System.out.println("方法被调用"); }
赋值调用 :调用方法,在方法前面定义变量,接收方法返回值
public static void main(String[] args) { int sum = getSum(5,6); System.out.println(sum); } public static int getSum(int a,int b) { return a + b; }
输出语句调用 :在输出语句中调用方法
public static void main(String[] args) { System.out.println(getSum(5,6)); } public static int getSum(int a,int b) { return a + b; }
注意事项
不能用输出语句调用void无返回值类型的方法。因为方法执行后没有结果,也就打印不出任何内容
public static void main(String[] args) { // 错误,不能输出语句调用void类型方法 System.out.println(printHello()); } public static void printHello() { System.out.println("Hello"); }
方法重载
方法重载概述
指在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可,与修饰符和返回值类型无关。 参数列表:个数不同,数据类型不同,顺序不同。 重载方法调用:JVM通过方法的参数列表,调用不同的方法。
注意事项
- 重载仅对应方法的定义,与方法的调用无关,调用方式参照标准格式
- 重载仅针对同一个类中方法的名称与参数进行识别,与返回值无关,换句话说不能通过返回值来判定两个方法是否相互构成重载
方法重载案例
案例一
需求
使用方法重载的思想,设计比较两个整数是否相同的方法,兼容全整数类型(byte,short,int,long)
思路
①定义比较两个数字的是否相同的方法compare()方法,参数选择两个int型参数
②定义对应的重载方法,变更对应的参数类型,参数变更为两个long型参数
③定义所有的重载方法,两个byte类型与两个short类型参数
④完成方法的调用,测试运行结果
代码实现
/** * 方法的重载 * 在一个类中,方法名相同,参数列表不同,与修饰符和返回值类型无关,这样的情况称之为方法重载 */ public class MethodTest1 { // 在一个类中,能不能有两个方法名相同的方法 答案:能 // 但是有要求,方法名相同,参数列表不同 public static void main(String[] args) { //调用方法 System.out.println(compare(10, 20)); System.out.println(compare((byte) 10, (byte) 20)); System.out.println(compare((short) 10, (short) 20)); System.out.println(compare(10L, 20L)); } //int public static boolean compare(int a, int b) { System.out.println("int"); return a == b; } //byte public static boolean compare(byte a, byte b) { System.out.println("byte"); return a == b; } //short public static boolean compare(short a, short b) { System.out.println("short"); return a == b; } //long public static boolean compare(long a, long b) { System.out.println("long"); return a == b; } }
案例二
判断哪些方法是重载关系
public static void open(){} public static void open(int a){} public static void open(int a,int b){} public static void open(double a,int b){} public static void open(int a,double b){} public void open(int i,double d){} public static void OPEN(){} public static void open(int i,int j){}
案例三
需求
模拟输出语句中的println方法的效果,传递什么类型的数据就输出什么类型的数据,只允许定义一个方法名println
代码实现
public class MethodTest2 { public static void println(byte a) { System.out.println(a); } public static void println(short a) { System.out.println(a); } public static void println(int a) { System.out.println(a); } public static void println(long a) { System.out.println(a); } public static void println(float a) { System.out.println(a); } public static void println(double a) { System.out.println(a); } public static void println(char a) { System.out.println(a); } public static void println(boolean a) { System.out.println(a); } public static void println(String a) { System.out.println(a); } }
方法的参数传递
方法参数传递基本类型
测试代码:
public class ArgsDemo01 { public static void main(String[] args) { int number = 100; System.out.println("调用change方法前:" + number); //100 change(number); System.out.println("调用change方法后:" + number); //100 } public static void change(int number) { number = 200; } }
结论:
基本数据类型的参数,形式参数的改变,不影响实际参数
结论依据:
每个方法在栈内存中,都会有独立的栈空间,方法运行结束后就会弹栈消失
方法参数传递引用类型
测试代码
public class ArgsDemo02 { public static void main(String[] args) { int[] arr = {10, 20, 30}; System.out.println("调用change方法前:" + arr[1]); //100 change(arr); System.out.println("调用change方法后:" + arr[1]); //200 } public static void change(int[] arr) { arr[1] = 200; } }
结论
对于引用类型的参数,形式参数的改变,影响实际参数的值
结论依据
引用数据类型的传参,传入的是地址值,内存中会造成两个引用指向同一个内存的效果,所以即使方法弹栈,堆内存中的数据也已经是改变后的结果
数组遍历
需求
设计一个方法用于数组遍历,要求遍历的结果是在一行上的。例如:[11, 22, 33, 44, 55]
思路
- 因为要求结果在一行上输出,所以这里需要在学习一个新的输出语句System.out.print(“内容”); System.out.println(“内容”); 输出内容并换行 System.out.print(“内容”); 输出内容不换行System.out.println(); 起到换行的作用
- 定义一个数组,用静态初始化完成数组元素初始化
- 定义一个方法,用数组遍历通用格式对数组进行遍历
- 用新的输出语句修改遍历操作
- 调用遍历方法
代码实现
public class MethodTest3 { public static void main(String[] args) { //定义一个数组,用静态初始化完成数组元素初始化 int[] arr = {11, 22, 33, 44, 55}; //调用方法 printArray(arr); } // 定义一个方法,用数组遍历通用格式对数组进行遍历 /* 两个明确: 返回值类型:void 参数:int[] arr */ public static void printArray(int[] arr) { System.out.print("["); for(int x=0; x<arr.length; x++) { if(x == arr.length-1) { System.out.print(arr[x]); } else { System.out.print(arr[x]+", "); } } System.out.println("]"); } }
数组最大值
需求
设计一个方法用于获取数组中元素的最大值
思路
① 定义一个数组,用静态初始化完成数组元素初始化
② 定义一个方法,用来获取数组中的最大值,最值的认知和讲解我们在数组中已经讲解过了
③ 调用获取最大值方法,用变量接收返回结果
④ 把结果输出在控制台
代码实现
public class MethodTest02 { public static void main(String[] args) { //定义一个数组,用静态初始化完成数组元素初始化 int[] arr = {12, 45, 98, 73, 60}; //调用获取最大值方法,用变量接收返回结果 int number = getMax(arr); //把结果输出在控制台 System.out.println("number:" + number); } //定义一个方法,用来获取数组中的最大值 /* 两个明确: 返回值类型:int 参数:int[] arr */ public static int getMax(int[] arr) { int max = arr[0]; for(int x=1; x<arr.length; x++) { if(arr[x] > max) { max = arr[x]; } } return max; } }
面向对象
目标
- 【理解】 什么是面向对象
- 【理解】 类和对象的关系
- 【掌握】 类的定义和使用
- 【理解】 对象的内存图
- 【掌握】 三大特征之封装
- 【掌握】 this关键字的使用
- 【掌握】 继承
- 【掌握】 抽象类
- 【理解】什么是接口
- 【掌握】接口的定义格式
- 【掌握】接口的使用
- 【理解】接口的成员特点
- 【理解】类和接口 抽象类和接口之间的关系
- 【掌握】单继承多实现
- 【理解】接口之间的多继承
- 【掌握】接口的案例
- 【理解】什么是动态
- 【理解】使用多态的前提
- 【掌握】多态的格式
- 【理解】多态中的成员访问特点
- 【理解】多态中的好处和弊端
- 【理解】多态中的转型
- 【理解】转型的异常
- 【掌握】综合案例
- 【理解】什么是内部类
- 【掌握】匿名内部类
- 【掌握】引用数据类型作为方法的参数
- 【理解】final关键字的使用
- 【理解】包的定义及使用
- 【理解】权限修饰符
- 【掌握】static关键字的使用
面向对象概述
什么是面向对象
Java语言是一种面向对象的程序设计语言,而面向对象思想是一种程序设计思想,我们在面向对象思想的指引下, 使用Java语言去设计、开发计算机程序。 这里的对象泛指现实中一切事物,每种事物都具备自己的属性和行为。面 向对象思想就是在计算机程序设计过程中,参照现实中事物,将事物的属性特征、行为特征抽象出来,描述成计算 机事件的设计思想。 它区别于面向过程思想,强调的是通过调用对象的行为来实现功能,而不是自己一步一步的去 操作实现。
面向过程与面向对象
面向过程
完成一个功能时,功能的每一个步骤,都需要我们进行参与,每一个细节都需要了解清楚,才能完成。强调的是过程。
面向对象
强调的是通过调用对象的行为来实现功能,而不是自己一步一步的去 操作实现
举例
- 洗衣服
- 面向过程
把衣服脱下来–>找一个盆–>放点洗衣粉–>加点水–>浸泡10分钟–>揉一揉–>清洗衣服–>拧干–>晾起来 - 面向对象
把衣服脱下来 –> 打开全自动洗衣机 –>扔衣服 –> 按钮 –> 晾起来
- 面向过程
- 吃饭
- 面向过程
买菜 –> 切菜 –> 洗菜 –> 上锅烧油 爆炒(调料) –> 上盘 –> 开吃 –>洗碗 - 面向对象
饭馆 –> 老板娘 来一盘饺子 –> 开吃
- 面向过程
面向对象的特点
- 符合了我们人类思考习惯的思想
- 将复杂的事情进行简单化了
- 将执行者变成了指挥者(角色发生了改变)
面向对象的三大特征
封装 继承 多态
小结
做事情 首先第一想到的是对象,那个对象能帮我们做事情 有对象: 直接用 没有对象: 我们自己造对象在进行使用
类和对象
什么是类
类是一组相关属性和行为的集合。可以看成是一类事物的模板,使用事物的属性特征和行为特征来描述该类事物。
- 现实中,描述一类事物:
- 属性:就是该事物的状态信息。
- 行为:就是该事物能够做什么。
- 属性:就是该事物的状态信息。
- 举例:小猫
- 属性:名字、体重、年龄、颜色
- 行为:走、跑、叫。
- 属性:名字、体重、年龄、颜色
什么是对象
对象:是一类事物的具体体现。对象是类的一个实例(对象并不是找个女朋友),必然具备该类事物的属性和行为
- 现实中,一类事物的一个实例:一只小花猫。
举例:一只小猫。
- 属性:tom、5kg、2 years、yellow。
- 行为:溜墙根走、蹦跶的跑、喵喵叫。
类与对象的关系
类是对一类事物的描述,是抽象的
对象是一类事物的实例,是具体的
类是对象的模板,对象是类的实体
类的定义和使用
类的定义
- 类的组成是由属性和行为两部分组成
- 属性:在类中通过成员变量来体现(类中方法外的变量)
- 行为:在类中通过成员方法来体现(和前面的方法相比去掉static关键字即可)
- 属性:在类中通过成员变量来体现(类中方法外的变量)
类的定义格式
public class ClassName { //成员变量 //成员方法 }
定义学生类
public class Student { //成员变量 String name;//姓名 int age;//年龄 // 成员方法 // 学习的方法 public void study() { System.out.println("好好学习,天天向上"); } //吃饭的方法 public void eat() { System.out.println("学习饿了要吃饭"); } }
对象的使用
创建对象格式
类名 对象名 = new 类名();
访问类中的成员
对象名.成员变量;
对象名.成员方法();
代码演示
public class Test01_Student { public static void main(String[] args) { //创建对象格式:类名 对象名 = new 类名(); Student s = new Student(); System.out.println("s:"+s); // //直接输出成员变量值 System.out.println("姓名:"+s.name); //null System.out.println("年龄:"+s.age); //0 System.out.println("‐‐‐‐‐‐‐‐‐‐"); //给成员变量赋值 s.name = "赵丽颖"; s.age = 18; //再次输出成员变量的值 System.out.println("姓名:"+s.name); //赵丽颖 System.out.println("年龄:"+s.age); //18 System.out.println("‐‐‐‐‐‐‐‐‐‐"); //调用成员方法 s.study(); // "好好学习,天天向上" s.eat(); // "学习饿了要吃饭" } }
成员变量的默认值
- 基本类型
- 整数(byte, short, int, long) 0
- 浮点数(float, double) 0.0
- 字符(char) '\u0000'
- 布尔(boolean) false
- 整数(byte, short, int, long) 0
- 引用类型
- 数组,类,接口 null
- 数组,类,接口 null
类与对象的练习
定义手机类
public class Phone { // 成员变量 String brand; //品牌 int price; //价格 String color; //颜色 // 成员方法 //打电话 public void call(String name) { System.out.println("给"+name+"打电话"); } //发短信 public void sendMessage() { System.out.println("群发短信"); } }
测试类
public class Test02Phone { public static void main(String[] args) { //创建对象 Phone p = new Phone(); //输出成员变量值 System.out.println("品牌:"+p.brand);//null System.out.println("价格:"+p.price);//0 System.out.println("颜色:"+p.color);//null System.out.println("‐‐‐‐‐‐‐‐‐‐‐‐"); //给成员变量赋值 p.brand = "锤子"; p.price = 2999; p.color = "棕色"; //再次输出成员变量值 System.out.println("品牌:"+p.brand);//锤子 System.out.println("价格:"+p.price);//2999 System.out.println("颜色:"+p.color); //棕色 System.out.println("‐‐‐‐‐‐‐‐‐‐‐‐"); //调用成员方法 p.call("紫霞"); p.sendMessage(); } }
对象内存图
一个对象内存图
多了一个方法区,用于存储class文件 的
两个对象的内存图
多个对象用的都是同一个方法区的内容,方法区中的class只有唯一的一份,同名的类
多个对象指向相同内存图
一个对象的改变会影响另外一个对象的改变
成员变量和局部变量的区别
- 在类中的位置不同
- 成员变量:类中,方法外
- 局部变量:方法中或者方法声明上(形式参数)
- 成员变量:类中,方法外
- 作用范围不一样
- 成员变量:类中
- 局部变量:方法中
- 成员变量:类中
- 初始化值的不同
- 成员变量:有默认值
- 局部变量:没有默认值。必须先定义,赋值,最后使用
- 成员变量:有默认值
- 在内存中的位置不同
- 成员变量:堆内存
- 局部变量:栈内存
- 成员变量:堆内存
- 生命周期不同
- 成员变量:随着对象的创建而存在,随着对象的消失而消失
- 局部变量:随着方法的调用而存在,随着方法的调用完毕而消失
- 成员变量:随着对象的创建而存在,随着对象的消失而消失
面向对象三大特征-封装
什么是封装
面向对象编程语言是对客观世界的模拟,客观世界里成员变量都是隐藏在对象内部的,外界无法直接操作和修改。 封装可以被认为是一个保护屏障,防止该类的代码和数据被其他类随意访问。要访问该类的数据,必须通过指定的 方式。适当的封装可以让代码更容易理解与维护,也加强了代码的安全性。
封装的原则
将 属性隐藏 起来,若需要访问某个属性, 提供公共方法 对其访问。
如何封装
- 使用 private 关键字来修饰成员变量。
- 对需要访问的成员变量,提供对应的一对 getXxx 方法 、 setXxx 方法。
private 关键字
什么是private关键字
- private是一个权限修饰符,代表最小权限。
- 可以修饰成员变量和成员方法。
- 被private修饰后的成员变量和成员方法,只在本类中才能访问
private的使用格式
private 数据类型 变量名 ;
使用 private 修饰成员变量,代码如下
public class Student { private String name; private int age; }
提供 getXxx 方法 / setXxx 方法,可以访问成员变量,代码如下
public class Student { private String name; private int age; public void setName(String n) { name = n; } public String getName() { return name; } public void setAge(int a) { age = a; } public int getAge() { return age; } }
this 关键字
问题描述
我们发现 setXxx 方法中的形参名字并不符合见名知意的规定,那么如果修改与成员变量名一致,是否就见名知意了呢?代码如下:
public class Student { private String name; private int age; public void setName(String name) { name = name; } public void setAge(int age) { age = age; } }
经过修改和测试,我们发现新的问题,成员变量赋值失败了。也就是说,在修改了 setXxx() 的形参变量名后,方法并没有给成员变量赋值!这是由于形参变量名与成员变量名重名,导致成员变量名被隐藏,方法中的变量名,无法访问到成员变量,从而赋值失败。所以,我们只能使用this关键字,来解决这个重名问题。
this 的含义
this代表所在类的当前对象的引用(地址值),即对象自己的引用。
记住 :方法被哪个对象调用,方法中的this就代表那个对象。即谁在调用,this就代表谁。
this 的使用格式
this.成员变量名;
代码修改
public class Student { private String name; private int age; public void setName(String name) { //name = name; this.name = name; } public String getName() { return name; } public void setAge(int age) { //age = age; this.age = age; } public int getAge() { return age; } }
小贴士:方法中只有一个变量名时,默认也是使用 this 修饰,可以省略不写。
小结
封装:
- 意义: 对类中的属性的访问权限进行控制,防止外部随意对属性的值进行修改
- 代码过程:
- 属性私有化 private int age;
- 生成get/set方法
- 属性私有化 private int age;
- this 代表当前对象。例如如果是s1.setName(),那么setName方法中的this就代表s1对象
构造方法
构造方法概述
当一个对象被创建时候,构造方法用来初始化该对象,给对象的成员变量赋初始值。
小贴士:无论你与否自定义构造方法,所有的类都有构造方法,因为Java自动提供了一个无参数构造方法,
一旦自己定义了构造方法,Java自动提供的默认无参数构造方法就会失效。
构造方法的定义格式
修饰符 构造方法名(参数列表){ // 方法体 }
构造方法的使用
构造方法的写法上,方法名与它所在的类名相同。它没有返回值,所以不需要返回值类型,甚至不需要void。使用构造方法后,代码如下
public class Student { private String name; private int age; // 无参数构造方法 public Student() {} // 有参数构造方法 public Student(String name,int age) { this.name = name; this.age = age; } }
构造方法的注意事项
- 如果你不提供构造方法,系统会给出无参数构造方法。
- 如果你提供了构造方法,系统将不再提供无参数构造方法。
- 构造方法是可以重载的,既可以定义参数,也可以不定义参数。
小结
构造方法
- public 类名(){} 【无参数构造方法】
- 构造方法是在new对象的时候被调用
- 一个类中可以不写构造方法,系统会自动生成一个无参数的构造方法
- 一个类中自己写了构造方法,如果还有需要,自己提供即可
- 构造方法也可以重载
标准JavaBean编写
JavaBean 是 Java语言编写类的一种标准规范。符合 JavaBean 的类,要求类必须是具体的和公共的,并且具有无
参数的构造方法,提供用来操作成员变量的 set 和 get 方法。
public class ClassName{ //成员变量 //构造方法 //无参构造方法【必须】 //有参构造方法【建议】 //成员方法 //getXxx() //setXxx() }
编写Student类
public class Student { //成员变量 private String name; private int age; //构造方法,这里用到了方法重载 public Student() {} public Student(String name,int age) { this.name = name; this.age = age; } //成员方法 public void setName(String name) { this.name = name; } public String getName() { return name; } publicvoid setAge(int age) { this.age = age; } publicint getAge() { return age; } }
测试
public class TestStudent { public static void main(String[] args) { //无参构造使用 Student s= new Student(); s.setName("柳岩"); s.setAge(18); System.out.println(s.getName()+"‐‐‐"+s.getAge()); //带参构造使用 Student s2= new Student("赵丽颖",18); System.out.println(s2.getName()+"‐‐‐"+s2.getAge()); } }
面向对象三大特征-继承
继承概述
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要
其中,多个类可以称为子类,单独那一个类称为 父类、超类(superclass)或者基类** 。
继承描述的是事物之间的所属关系,这种关系是: is-a 的关系。例如,图中兔子属于食草动物,食草动物属于动物。可见,父类更通用,子类更具体。我们通过继承,可以使多种事物之间形成一种关系体系。
继承概念
继承:就是子类继承父类的 属性和行为 ,使得子类对象具有与父类相同的属性、相同的行为。子类可以直接访问父类中的 非私有 的属性和行为。
继承的特点
- 子类自动拥有父类非私有的成员
- 子类也可以拥有自己的成员
继承的好处
- 提高代码的复用性。
- 类与类之间产生了关系,是多态的前提。
继承的格式
通过 extends 关键字,可以声明一个子类继承另外一个父类,定义格式如下:
格式
class 父类 { ... } class 子类 extends 父类 { ... }
代码演示
/** 定义员工类Employee,做为父类 */ class Employee { String name; // 定义name属性 // 定义员工的工作方法 public void work() { System.out.println("尽心尽力地工作"); } } /* * 定义讲师类Teacher 继承 员工类Employee */ class Teacher extends Employee { // 定义一个打印name的方法 public void printName() { System.out.println("name=" + name); } } /* * 定义测试类 */ public class ExtendDemo01 { public static void main(String[] args) { // 创建一个讲师类对象 Teacher t = new Teacher(); // 为该员工类的name属性进行赋值 t.name = "小明"; // 调用该员工的printName()方法 t.printName(); // name = 小明 // 调用Teacher类继承来的work()方法 t.work(); // 尽心尽力地工作 } }
继承中成员访问的特点
成员变量
成员变量不重名
如果子类父类中出现不重名的成员变量,这时的访问是没有影响的。代码如下:
class Fu { // Fu中的成员变量。 int num = 5; } class Zi extends Fu { // Zi中的成员变量 int num2 = 6; // Zi中的成员方法 public void show() { // 访问父类中的num, System.out.println("Fu num="+num); // 继承而来,所以直接访问。 // 访问子类中的num2 System.out.println("Zi num2="+num2); } } class ExtendDemo02 { public static void main(String[] args) { // 创建子类对象 Zi z = new Zi(); // 调用子类中的show方法 z.show(); } }
成员变量重名
如果子类父类中出现重名的成员变量,这时的访问是有影响的。代码如下
class Fu { // Fu中的成员变量。 int num = 5; } class Zi extends Fu { // Zi中的成员变量 int num = 6; public void show() { // 访问父类中的num System.out.println("Fu num=" + num); // 访问子类中的num System.out.println("Zi num=" + num); } } class ExtendsDemo03 { public static void main(String[] args) { // 创建子类对象 Zi z = new Zi(); // 调用子类中的show方法 z.show(); } }
子父类中出现了同名的成员变量时,在子类中需要访问父类中非私有成员变量时,需要使用super关键字,修饰父类成员变量,类似于之前学过的this
使用格式
super.父类成员变量名
代码演示
class Zi extends Fu { // Zi中的成员变量 int num = 6; public void show() { //访问父类中的num System.out.println("Fu num=" + super.num); //访问子类中的num System.out.println("Zi num=" + this.num); } }
注意
Fu类中的成员变量是非私有的,子类中可以直接访问。若Fu类中的成员变量私有了,子类是不能直接访问的。通常编码时,我们遵循封装的原则,使用private修饰成员变量,那么如何访问父类的私有成员变量呢?对!可以在父类中提供公共的getXxx方法和setXxx方法。
成员方法
成员方法不重名
如果子类父类中出现不重名的成员方法,这时的调用是没有影响的。对象调用方法时,会先在子类中查找有没有对应的方法,若子类中存在就会执行子类中的方法,若子类中不存在就会执行父类中相应的方法。代码如下:
class Fu{ public void show(){ System.out.println("Fu类中的show方法执行"); } } class Zi extends Fu{ public void show2(){ System.out.println("Zi类中的show2方法执行"); } } public class ExtendsDemo04{ public static void main(String[] args) { Zi z = new Zi(); //子类中没有show方法,但是可以找到父类方法去执行 z.show(); z.show2(); } }
成员方法重名(重写(Override))
如果子类父类中出现重名的成员方法,这时的访问是一种特殊情况,叫做方法重写(Override)。
什么是方法的重写
方法重写:子类中出现与父类一模一样的方法时(返回值类型,方法名和参数列表都相同),会出现覆盖效
果,也称为重写或者复写。声明不变,重新实现。
代码演示
class Fu { public void show() { System.out.println("Fu show"); } } class Zi extends Fu { //子类重写了父类的show方法 public void show() { System.out.println("Zi show"); } } public class ExtendsDemo05{ public static void main(String[] args) { Zi z = new Zi(); // 子类中有show方法,只执行重写后的show方法 z.show(); // Zi show } }
方法重写的应用场景
子类可以根据需要,定义特定于自己的行为。既沿袭了父类的功能名称,又根据子类的需要重新实现父类方法,从而进行扩展增强。比如新的手机增加来电显示头像的功能,代码如下
代码演示
class Phone { public void sendMessage(){ System.out.println("发短信"); } public void call(){ System.out.println("打电话"); } public void showNum(){ System.out.println("来电显示号码"); } } //智能手机类 class NewPhone extends Phone { //重写父类的来电显示号码功能,并增加自己的显示姓名和图片功能 public void showNum(){ //调用父类已经存在的功能使用super super.showNum(); //增加自己特有显示姓名和图片功能 System.out.println("显示来电姓名"); System.out.println("显示头像"); } } public class ExtendsDemo06 { public static void main(String[] args) { // 创建子类对象 NewPhone np = new NewPhone(); // 调用父类继承而来的方法 np.call(); // 调用子类重写的方法 np.showNum(); } }
注意
这里重写时,用到super.父类成员方法,表示调用父类的成员方法
方法重写的注意事项
- 子类方法覆盖父类方法,必须要保证权限大于等于父类权限。
- 子类方法覆盖父类方法,返回值类型、函数名和参数列表都要一模一样。
构造方法执行特点
描述
1.构造方法的名字是与类名一致的。所以子类是无法继承父类构造方法的。
2.构造方法的作用是初始化成员变量的。所以子类的初始化过程中,必须先执行父类的初始化动作。子类的构造方法中默认有一个super() , 表示调用父类的构造方法,父类成员变量初始化后,才可以给子类使用。
代码演示
class Fu { private int n; Fu(){ System.out.println("Fu()"); } } class Zi extends Fu { Zi(){ // super(),调用父类构造方法 super(); System.out.println("Zi()"); } } public class ExtendsDemo07{ public static void main (String args[]){ Zi zi = new Zi(); } }
在每次创建子类对象时,先初始化父类空间,再创建其子类对象本身。目的在于子类对象中包含了其对应的父类空间,便可以包含其父类的成员,如果父类成员非private修饰,则子类可以随意使用父类成员。代码体现在子类的构造方法调用时,一定先调用父类的构造方法。
访问构造方法的注意事项
- 在子类构造方法中的第一行默认有 super() ,给父类进行初始化的
在子类构造方法中可以手动调用父类其他重载的构造方法
格式: super(参数列表);
- super 是写在子类的构造方法的第一行.
super和this
super和this的含义
- super:代表父类的存储空间标识(可以理解为父亲的引用)。
- this:代表当前对象的引用(谁调用就代表谁)。
super和this的用法
访问成员
this.成员变量 ‐‐ 本类的 super.成员变量 ‐‐ 父类的 this.成员方法名() ‐‐ 本类的 super.成员方法名() ‐‐ 父类的
代码演示
class Animal { public void eat() { System.out.println("animal : eat"); } } class Cat extends Animal { public void eat() { System.out.println("cat : eat"); } public void eatTest() { this.eat(); // this 调用本类的方法 super.eat(); // super 调用父类的方法 } } public class ExtendsDemo08 { public static void main(String[] args) { Animal a = new Animal(); a.eat(); Cat c = new Cat(); c.eatTest(); } } }
访问构造
this(...) ‐‐ 本类的构造方法 super(...) ‐‐ 父类的构造方法
子类的每个构造方法中均有默认的super(),调用父类的空参构造。手动调用父类构造会覆盖默认的super()。
super()和this()都必须是在构造方法的第一行,所以不能同时出现。
继承的特点
Java只支持单继承,不支持多继承
//一个类只能有一个父类,不可以有多个父类。 class C extends A{} //ok class C extends A,B... //error
Java支持多层继承(继承体系)
class A{} class B extends A{} class C extends B{}
- 子类和父类是一种相对的概念
一个父类可以有很多个子类
class Animal{} class Dog extends Animal{} class Cat extends Animal{} class Pig extends Animal{}
小结
继承
- public class 子类 extends 父类
- 子类就可以继承父类的非private修饰的成员变量和方法
- super关键字
- super() 调用父类构造方法
- super.属性名 调用父类的属性
- super.方法名 调用父类的方法
- super() 调用父类构造方法
- java是单继承
抽象类
父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了。我们把没有方法主体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类。
什么是抽象
什么是抽象方法
只有方法的声明,没有方法体的方法,就是抽象方法
什么是抽象类
抽象方法所在的类必定是一个抽象类
抽象类和抽象方法的格式
- 抽象方法
使用abstract关键字修饰方法,该方法就成了抽象方法,抽象方法只包含一个方法名,而没有方法体。
定义格式
修饰符 abstract 返回值类型 方法名 (参数列表);
演示
public abstract void run();
- 抽象类
如果一个类包含抽象方法,那么该类必须是抽象类。
定义格式
public abstract class 类名字 { }
演示
public abstract class Animal { public abstract void run(); }
抽象的使用
继承抽象类的子类 必须重写父类所有的抽象方法 。否则,该子类也必须声明为抽象类。最终,必须有子类实现该父类的抽象方法,否则,从最初的父类到最终的子类都不能创建对象,失去意义。
代码演示
public abstract class Animal { public abstract void run(); } public class Cat extends Animal { public void run (){ System.out.println("小猫在墙头走~~~"); } } public class CatTest { public static void main(String[] args) { // 创建子类对象 Cat c = new Cat(); // 调用run方法 c.run(); } }
此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法。
抽象的注意事项
- 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。
理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。 - 抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。
理解:子类的构造方法中,有默认的super(),需要访问父类构造方法。 - 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。 - 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类
理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。
小结
抽象类
- public abstract class 类名
- public abstract 返回值类型 方法名
- 抽象方法所在的类必须是抽象类
- 子类必须实现(重写)抽象类中的方法,否则会报错
- 抽象类不能new对象,但是抽象类肯定会有非抽象类的子类,可以创建子类的对象
- 抽象类的意义是什么:父类当中可以定义一些子类共同的方法,由子类继承使用,但是有一些方法,每个子类都会从现有自己具体的实现,此时父类中对应的方法,就没有必要实现了,可以将该就方法定义为抽象方法,此时类就必须是抽象类。
接口
接口基本概述及格式
- 接口概述
接口,是Java语言中一种引用类型,是方法的集合,如果说类的内部封装了成员变量、构造方法和成员方法,那么接口的内部主要就是封装了方法,包含抽象方法 (JDK7及以前) , 默认方法和静态方法(JDK8)私有方法(JDK9)。
总结
接口就是一种公共的规范标准,只要符合规范标准,大家都可以通用。Java中的接口更多的体现在对行为的抽象!
- 接口定义格式
接口用关键字interface修饰
public interface 接口名 {}
- 接口的使用
接口是不能创建对象,必须有实现类才能使用,类实现接口用implements表示
public class 类名 implements 接口名 {}
注意: 接口的实现类必须重写接口中的所有的抽象方法,要么该类是一个抽象类
接口成员的特点
- 成员变量
只能是常量,默认修饰符:public static final
- 成员方法
只能是抽象方法,默认修饰符:public abstract
- 构造方法
没有,因为接口主要是扩展功能的,而没有具体存在
代码演示
- 接口
public interface Inter { public int num = 10; public final int num2 = 20; // public static final int num3 = 30; int num3 = 30; //public Inter() {} //public void show() {} public abstract void method(); void show(); }
- 实现类
public class InterImpl implements Inter { public InterImpl() { super(); } @Override public void method() { System.out.println("method"); } @Override public void show() { System.out.println("show"); } }
- 测试类
public class InterfaceDemo { public static void main(String[] args) { Inter i = new InterImpl(); // i.num = 20; System.out.println(i.num); // i.num2 = 40; System.out.println(i.num2); System.out.println(Inter.num); } }
类和接口的关系
- 类与类的关系
继承关系,只能单继承,但是可以多层继承 - 类与接口的关系
实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口 - 接口与接口的关系
继承关系,可以单继承,也可以多继承
抽象类和接口的区别
- 成员区别
- 抽象类
变量,常量;有构造方法;有抽象方法,也有非抽象方法 - 接口
常量;抽象方法
- 抽象类
- 关系区别
- 类与类
继承,单继承 extends - 类与接口
实现,可以单实现,也可以多实现 implements - 接口与接口
继承,单继承,多继承
- 类与类
- 设计理念区别
- 抽象类
为了继承而来,让子类强制重写父类中的抽象方法 - 接口
对行为抽象,主要是行为
- 抽象类
继承父类并实现多个接口
之前学过,在继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现。并且,一个类能继承一个父类,同时实现多个接口。
多实现格式
class 类名 [extends 父类名] implements 接口名1,接口名2,接口名3... { // 重写接口中抽象方法【必须】 // 重写接口中默认方法【不重名时可选】 }
代码演示
定义接口
interface A { public abstract void showA(); public abstract void show(); } interface B { public abstract void showB(); public abstract void show(); }
定义父类
public class Fu{}
定义实现类
public class C extends Fu implements A,B{ @Override public void showA() { System.out.println("showA"); } @Override public void showB() { System.out.println("showB"); } @Override public void show() { System.out.println("show"); } }
注意事项
接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次即可!
如果实现类继承了父类,这个父类是一个抽象类时,我们还需要再重写抽象类中的所有抽象方法。
接口之间的多继承
一个接口能继承另一个或者多个接口,这和类之间的继承比较相似。接口的继承使用extends关键字,子接口继承父接口的方法。如果父接口中的默认方法有重名的,那么子接口需要重写一次。
定义父接口
interface A { public void method1(); } interface B { public void method2(); }
定义子接口
interface D extends A,B{ public void method2(); }
注意
接口多继承之后,如果想使用,我们还必须定义实现类,才能使用
接口小结
- 接口中只有常量和抽象方法
- 接口是没有静态代码块和构造方法的。
一个类的直接父类是唯一的,但是一个类可以同时实现多个接口。 单继承多实现
public class MyInterfaceImpl implements MyInterfaceA, MyInterfaceB { // 覆盖重写所有抽象方法 }
- 如果实现类所实现的多个接口当中,存在重复的抽象方法,那么只需要覆盖重写一次即可。
- 如果实现类没有覆盖重写所有接口当中的所有抽象方法,那么实现类就必须是一个抽象类。
接口案例
需求
对猫和狗进行训练,他们就可以跳高了,这里加入跳高功能。
请采用抽象类和接口来实现猫狗案例,并在测试类中进行测试。
- 代码实现
Animal类
public abstract class Animal { private String name; private int age; public Animal() { } public Animal(String name, int age) { this.name = name; this.age = age; } 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; } public abstract void eat(); }
跳高接口Jumping
public interface Jumpping { public abstract void jump(); }
猫类(Cat)
public class Cat extends Animal implements Jumpping { public Cat() { } public Cat(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("猫吃鱼"); } @Override public void jump() { System.out.println("猫可以跳高了"); } }
测试类
public class AnimalDemo { public static void main(String[] args) { Cat c = new Cat(); c.setName("加菲"); c.setAge(5); System.out.println(c.getName()+","+c.getAge()); c.eat(); c.jump(); } }
面向对象三大特征-多态
什么是多态
多态是继封装、继承之后,面向对象的第三大特性。【最难理解】
生活中,比如跑的动作,小猫、小狗和大象,跑起来是不一样的。再比如飞的动作,昆虫、鸟类和飞机,飞起来也
是不一样的。可见,同一行为,通过不同的事物,可以体现出来的不同的形态。多态,描述的就是这样的状态。
定义:同一个对象,在不同时刻表现出来的形态是不同的
多态的前提
- 要有继承或实现关系
- 要有方法的重写
- 要有父类引用指向子类对象
多态格式
普通类多态的格式
父类 对象 = new 子类(); // 创建子类对象,存储在父类类型的变量中
抽象类多态的格式
抽象类 对象名 = new 抽象类子类();
接口多态的格式
接口 对象名 = new 接口实现类();
多态中的成员访问特点
- 成员变量
编译看父类,运行看父类
- 成员方法
编译看父类,运行看子类
- 代码演示
动物类(Animal)
public class Animal { public int age = 40; public void eat() { System.out.println("动物吃东西"); } }
猫类(Cat)
public class Cat extends Animal { public int age = 20; public int weight = 10; @Override public void eat() { System.out.println("猫吃鱼"); } public void playGame() { System.out.println("猫捉迷藏"); } }
测试类
public class AnimalDemo { public static void main(String[] args) { //有父类引用指向子类对象 Animal a = new Cat(); System.out.println(a.age); // System.out.println(a.weight); a.eat(); // a.playGame(); } }
- 【应用场景1】多态在参数方面的应用:
Demo4.java
package day4; import java.util.Scanner; //抽象父类 abstract class Person{ public abstract void eat(); } //学生类 class Student extends Person{ @Override public void eat() { System.out.println("学生吃东西"); } public void kk(){ System.out.println("kk..."); } } //工人类 class Worker extends Person{ @Override public void eat() { System.out.println("工人吃东西"); } } class Driver extends Person{ @Override public void eat() { System.out.println("司机吃东西"); } } public class Demo4 { public static void main(String[] args) { System.out.println("请输入你身份"); Scanner in = new Scanner(System.in); int i = in.nextInt(); //父类类型 变量名 = new 子类实例(); Person person = null; if(i == 1){ person = new Student(); }else if(i == 2){ person = new Worker(); }else if(i == 3){ person = new Driver(); } m1(person); } public static void init(){ System.out.println("初始化系统"); } public static void go(){ System.out.println("系统继续运行..."); } public static void m1(Person person){ init(); person.eat(); go(); } }
- 【应用场景2】多态在返回值上的应用:
Demo5.java
package day4; import java.util.Scanner; public class Demo5 { public static void main(String[] args) { Scanner in = new Scanner(System.in); int j = in.nextInt(); Person person = m1(j); if(person instanceof Student){ Student s = (Student) person; s.kk(); }else if(person instanceof Worker){ } } public static Person m1(int i){ Person person = null; if(i == 1){ person = new Worker(); }else if(i == 2){ person = new Student(); }else if(i == 3){ person = new Driver(); } return person; } /*public static Worker m1(){ return new Worker(); } public static Student m2(){ return new Student(); } public static Driver m3(){ return new Driver(); }*/ }
小结
多态
- 前提:继承
- 父类引用指向子类的实例
父类 x = new 子类();
- 【应用场景1】多态在参数方面的应用:
- 方法的参数是父类引用
- 调用方法的时候,就可以传递任意子类实例
- 方法的参数是父类引用
- 【应用场景2】多态在返回值上的应用:
- 方法的返回值类型如果是父类,方法的返回值可以是任意子类对象
- 方法的返回值类型如果是父类,方法的返回值可以是任意子类对象
- instanceof 强制类型转换
- Person p1 =new Student();
- p1 不能调用Student子类中独有的方法。如果希望调用,则需要转换类型
- p1 instanceof 类型 判断p1中存储的对象是否是该类型
- Student s = (Student)p1; 将p1转换成Student类型
- Person p1 =new Student();
类型转换实例
Demo6.java
package day4; public class Demo6 { public static void main(String[] args) { //用普通的方式new对象,和用多态的方式new对象,有啥区别? //用多态的方式new对象,只能调用子类中继承或者重写的方法,而不能调用子类中独有方法 Student s1 = new Student(); s1.eat(); s1.kk(); Person p1 = new Student(); //p1不能调用kk方法,其实原因是p1可以存储Person任意子类对象,并不能确定是哪个子类, // 所以不让调用子类特有的方法 //如果现在确定p1存储的是Student对象,可以通过强制类型转换,然后再调用kk方法 //p1.kk(); if(p1 instanceof Student){ //判断p1中存储的对象是否是Student类型 //如果p1里面存储的对象是Student类型,则执行if中的代码 //将p1强制类型转换成Student对象,然后就可以通过转换后的对象调用Student独有的方法 Student s2 = (Student)p1; s2.kk(); } } }
多态的好处和弊端
- 多态的好处
提高程序的扩展性。定义方法时候,使用父类型作为参数,在使用的时候,使用具体的子类型参与操作
多态的弊端
不能使用子类的特有成员
- 多态的好处代码演示
实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利
定义父类
public abstract class Animal { public abstract void eat(); }
定义子类
class Cat extends Animal { public void eat() { System.out.println("吃鱼"); } } class Dog extends Animal { public void eat() { System.out.println("吃骨头"); } }
定义测试类
public class Test { public static void main(String[] args) { // 多态形式,创建对象 Cat c = new Cat(); Dog d = new Dog(); // 调用showCatEat showCatEat(c); // 调用showDogEat showDogEat(d); /* 以上两个方法, 均可以被showAnimalEat(Animal a)方法所替代 而执行效果一致 */ showAnimalEat(c); showAnimalEat(d); } public static void showCatEat (Cat c){ c.eat(); } public static void showDogEat (Dog d){ d.eat(); } public static void showAnimalEat (Animal a){ a.eat(); } }
- 多态的好处小结
由于多态特性的支持,showAnimalEat方法的Animal类型,是Cat和Dog的父类类型,父类类型接收子类对象,当然可以把Cat对象和Dog对象,传递给方法。当eat方法执行时,多态规定,执行的是子类重写的方法,那么效果自然与showCatEat、showDogEat方法一致,所以showAnimalEat完全可以替代以上两方法。
不仅仅是替代,在扩展性方面,无论之后再多的子类出现,我们都不需要编写showXxxEat方法了,直接使用showAnimalEat都可以完成。
所以,多态的好处,体现在,可以使程序编写的更简单,并有良好的扩展。
多态中的转型
为什么要用转型
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做向下转型。
- 向上转型
多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。
使用格式
父类类型 变量名 = new 子类类型(); 如:Animal a = new Cat();
- 向下转型
父类类型向子类类型向下转换的过程,这个过程是强制的。
一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。
使用格式
子类类型 变量名 = (子类类型) 父类变量名; 如:Cat c =(Cat) a;
- 代码演示
定义类
abstract class Animal { abstract void eat(); } class Cat extends Animal { public void eat() { System.out.println("吃鱼"); } public void catchMouse() { System.out.println("抓老鼠"); } } class Dog extends Animal { public void eat() { System.out.println("吃骨头"); } public void watchHouse() { System.out.println("看家"); } }
测试类
public class Test { public static void main(String[] args) { // 向上转型 Animal a = new Cat(); a.eat(); // 调用的是 Cat 的 eat // 向下转型 Cat c = (Cat)a; c.catchMouse(); // 调用的是 Cat 的 catchMouse } }
转型的异常
问题描述
转型的过程中,一不小心就会遇到这样的问题,请看如下代码:
public class Test { public static void main(String[] args) { // 向上转型 Animal a = new Cat(); a.eat(); // 调用的是 Cat 的 eat // 向下转型 Dog d = (Dog)a; d.watchHouse(); // 调用的是 Dog 的 watchHouse 【运行报错】 } }
这段代码可以通过编译,但是运行时,却报出了ClassCastException类型转换异常!这是因为,明明创建了
Cat类型对象,运行时,当然不能转换成Dog对象的。这两个类型并没有任何继承关系,不符合类型转换的定义。
为了避免ClassCastException的发生,Java提供了instanceof关键字,给引用变量做类型的校验,格式如下:
instanceof使用格式
变量名 instanceof 数据类型 如果变量属于该数据类型,返回true。 如果变量不属于该数据类型,返回false。
instanceof代码演示
public class Test { public static void main(String[] args) { // 向上转型 Animal a = new Cat(); a.eat(); // 调用的是 Cat 的 eat // 向下转型 if (a instanceof Cat){ Cat c = (Cat)a; c.catchMouse(); // 调用的是 Cat 的 catchMouse } else if (a instanceof Dog){ Dog d = (Dog)a; d.watchHouse(); // 调用的是 Dog 的 watchHouse } } }
综合案例
案例需求
我们现在有乒乓球运动员和篮球运动员,乒乓球教练和篮球教练。
为了出国交流,跟乒乓球相关的人员都需要学习英语。
请用所学知识分析,这个案例中有哪些具体类,哪些抽象类,哪些接口,并用代码实现。
- 抽象类 人: 姓名、年龄、吃饭()
- 抽象类 运动员:学习()
- 具体类 篮球运动员
- 具体类 乒乓球运动员
- 具体类 篮球运动员
- 抽象类 教练:教()
- 具体类 乒乓球教练类
- 具体类篮球教练类
- 具体类 乒乓球教练类
- 抽象类 运动员:学习()
- 接口 学习英语
- 具体类 乒乓球运动员
- 具体类 乒乓球教练类
- 具体类 乒乓球运动员
- 代码实现
Person类
public abstract class Person { private String name; private int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } 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; } public abstract void eat(); }
抽象运动员类(Player)
public abstract class Player extends Person { public Player() { } public Player(String name, int age) { super(name, age); //调用父类的有参构造方法 } public abstract void study(); }
抽象教练类(Coach)
public abstract class Coach extends Person { public Coach() { } public Coach(String name, int age) { super(name, age); } public abstract void teach(); }
学英语接口(SpeakEnglish)
public interface SpeakEnglish { public abstract void speak(); }
蓝球教练(BasketballCoach)
public class BasketballCoach extends Coach { public BasketballCoach() { } public BasketballCoach(String name, int age) { super(name, age); } @Override public void teach() { System.out.println("篮球教练教如何运球和投篮"); } @Override public void eat() { System.out.println("篮球教练吃羊肉,喝羊奶"); } }
乒乓球教练(PingPangCoach)
public class PingPangCoach extends Coach implements SpeakEnglish { public PingPangCoach() { } public PingPangCoach(String name, int age) { super(name, age); } @Override public void teach() { System.out.println("乒乓球教练教如何发球和接球"); } @Override public void eat() { System.out.println("乒乓球教练吃小白菜,喝大米粥"); } @Override public void speak() { System.out.println("乒乓球教练说英语"); } }
乒乓球运动员(PingPangPlayer)
public class PingPangPlayer extends Player implements SpeakEnglish { public PingPangPlayer() { } public PingPangPlayer(String name, int age) { super(name, age); } @Override public void study() { System.out.println("乒乓球运动员学习如何发球和接球"); } @Override public void eat() { System.out.println("乒乓球运动员吃大白菜,喝小米粥"); } @Override public void speak() { System.out.println("乒乓球运动员说英语"); } }
篮球运动员(BasketballPlayer)
public class BasketballPlayer extends Player { public BasketballPlayer() { } public BasketballPlayer(String name, int age) { super(name, age); } @Override public void study() { System.out.println("篮球运动员学习如何运球和投篮"); } @Override public void eat() { System.out.println("篮球运动员吃牛肉,喝牛奶"); } }
内部类
什么是内部类
将一个类A定义在另一个类B里面,里面的那个类A就称为内部类,B则称为外部类。
内部类的分类
成员内部类
局部内部类
匿名内部类
成员内部类
在描述事物时,若一个事物内部还包含其他事物,就可以使用内部类这种结构。比如,汽车类 Car 中包含发动机
类 Engine ,这时, Engine 就可以使用内部类来描述,定义在成员位置。
成员内部类格式
class 外部类 { class 内部类{ } }
成员内部类访问特点
- 内部类可以直接访问外部类的成员,包括私有成员。
- 外部类要访问内部类的成员,必须要建立内部类的对象。
创建内部类对象格式
外部类名.内部类名 对象名 = new 外部类型().new 内部类型();
代码演示
定义类
public class Person { private boolean live = true; class Heart { public void jump() { // 直接访问外部类成员 if (live) { System.out.println("心脏在跳动"); }else { System.out.println("心脏不跳了"); } } } public boolean isLive() { return live; } public void setLive(boolean live) { this.live = live; } }
测试类
public class InnerDemo { public static void main(String[] args) { // 创建外部类对象 Person p = new Person(); // 创建内部类对象 Heart heart = p.new Heart(); // 调用内部类方法 heart.jump(); // 调用外部类方法 p.setLive(false); // 调用内部类方法 heart.jump(); } }
内部类仍然是一个独立的类,在编译之后会内部类会被编译成独立的.class文件,但是前面冠以外部类的类名和$符号。比如,Person$Heart.class
局部内部类
什么是局部内部类
将一个类定义在一个方法中,该类就称之为是局部内部类
如何使用局部内部类
只能在该方法内部使用局部内部类
代码演示
public class Test { public static void main(String[] args) { Outer outer = new Outer(); outer.show(); } } public class Outer { public void show(){ // 局部内部类 class Inner{ public void method(){ System.out.println("Inner 局部内部类的method方法执行了...."); } } // 在外部类的方法内使用局部内部类 Inner inner = new Inner(); inner.method(); } }
匿名内部类
是内部类的简化写法。它的本质是一个 带具体实现的 父类或者父接口的 匿名的子类对象。
开发中,最常用到的内部类就是匿名内部类了。以接口举例,当你使用一个接口时,似乎得做如下几步操作,
1. 定义子类 2. 重写接口中的方法 3. 创建子类对象 4. 调用重写后的方法
我们的目的,最终只是为了调用方法,那么能不能简化一下,把以上四步合成一步呢?匿名内部类就是做这样的快捷方式
使用匿名内部类的前提
匿名内部类必须 继承一个父类 或者 实现一个父接口 。
匿名内部类的格式
new 父类名或者接口名(){ // 方法重写 @Override public void method() { // 执行语句 } };
- 匿名内部类的使用
以接口为例,匿名内部类的使用,代码如下:
定义接口
public interface FlyAble{ public abstract void fly(); }
创建匿名内部类,并调用
public class InnerDemo { public static void main(String[] args) { /* 1.等号右边:是匿名内部类,定义并创建该接口的子类对象 2.等号左边:是多态赋值,接口类型引用指向子类对象 */ FlyAble f = new FlyAble(){ public void fly() { System.out.println("我飞了~~~"); } }; //调用 fly方法,执行重写后的方法 f.fly(); } }
常在方法的形式参数是接口或者抽象类时,也可以将匿名内部类作为参数传递。代码如下:
public class InnerDemo2 { public static void main(String[] args) { /* 1.等号右边:定义并创建该接口的子类对象 2.等号左边:是多态,接口类型引用指向子类对象 */ FlyAble f = new FlyAble(){ public void fly() { System.out.println("我飞了~~~"); } }; // 将f传递给showFly方法中 showFly(f); } public static void showFly(FlyAble f) { f.fly(); } }
以上两步,也可以简化为一步,代码如下
public class InnerDemo3 { public static void main(String[] args) { /* 创建匿名内部类,直接传递给showFly(FlyAble f) */ showFly( new FlyAble(){ public void fly() { System.out.println("我飞了~~~"); } }); } public static void showFly(FlyAble f) { f.fly(); } }
引用类型作为方法的参数和返回值
实际的开发中,引用类型的使用非常重要,也是非常普遍的。我们可以在理解基本类型的使用方式基础上,进一步去掌握引用类型的使用方式。基本类型可以作为成员变量、作为方法的参数、作为方法的返回值,那么当然引用类型也是可以的
class作为方法的参数及返回值
- 类名作为方法的形参
方法的形参是类名,其实需要的是该类的对象
实际传递的是该对象的【地址值】 - 类名作为方法的返回值
方法的返回值是类名,其实返回的是该类的对象
实际传递的,也是该对象的【地址值】 - 代码演示
Student类
public class Student { // 属性 private String name; private int age; // 构造方法 // setter 和 getter方法 }
测试类
public class Test { public static void main(String[] args) { Student stu = new Student("张三", 13); System.out.println(stu); useStudent(stu); System.out.println("~~~~~~~~~~~~~~~~~~~"); Student student = getStudent(); System.out.println(student); student.show(); } /** 类名作为方法的返回值和参数,要的是该类型的对象 */ public static void useStudent(Student student) { System.out.println(student); student.show(); } public static Student getStudent() { // 创建学生并返回 Student stu = new Student("李四", 14); System.out.println(stu); return stu; } }
抽象类作为方法的参数及返回值
抽象类作为形参和返回值
- 方法的形参是抽象类名,其实需要的是该抽象类的子类对象
- 方法的返回值是抽象类名,其实返回的是该抽象类的子类对象
代码演示
多态
abstract class Animal { public abstract void eat(); } class Cat extends Animal { @Override public void eat() { System.out.println("猫吃鱼"); } } public class AnimalDemo { public static void main(String[] args) { Animal a = new Cat(); useAnimal(a); Animal a2 = getAnimal(); //new Cat() a2.eat(); } public static void useAnimal(Animal a) { //Animal a = newCat(); a.eat(); } public static Animal getAnimal() { Animal a = new Cat(); return a; } }
接口作为方法的参数及返回值
接口作为形参和返回值
- 方法的形参是接口名,其实需要的是该接口的实现类对象
- 方法的返回值是接口名,其实返回的是该接口的实现类对象
代码演示
interface Jumpping { void jump(); } class Cat implements Jumpping { @Override public void jump() { System.out.println("猫可以跳高了"); } } public class JumppingDemo { public static void main(String[] args) { Jumpping j = new Cat(); useJumpping(j); Jumpping j2 = getJumpping(); //new Cat() j2.jump(); } public static void useJumpping(Jumpping j) { //Jumpping j= new Cat(); j.jump(); } public static Jumpping getJumpping() { Jumpping j = new Cat(); return j; } }
小结
引用数据类型作为参数和返回值
- 什么是引用数据类型
- 在java中除了8个基本数据以外,其它都是引用数据类型
- 8基本数据类型 byte short int long boolean char float double
- 数据类型 变量名 = …
- 数组 int[] arr = new int[3];
- 字符串 String s1 = "xx";
- Person/Student/JDK自带类
- Person p1 = new Person()
- Scaner input = …
- Person p1 = new Person()
- 数组 int[] arr = new int[3];
- 在java中除了8个基本数据以外,其它都是引用数据类型
- 引用数据类型作为方法的参数
- 定义 void test1(Student s1);
- 调用 test1(new Student())
- 定义 void test1(Student s1);
- 引用数据类型作为方法的返回值
- 定义 public Student m1(){ Student s1 = new Student(); return s1; }
- 调用 Student s2 = m1();
- 定义 public Student m1(){ Student s1 = new Student(); return s1; }
- 抽象类作为方法的参数和返回值【结合多态】
- 当一个方法的参数是抽象类时,调用该方法,可以传递抽象类的子类对象
- 当一个方法的返回值类型是抽象类时,该方法内部可以创建该抽象类子类对象
- 当一个方法的参数是抽象类时,调用该方法,可以传递抽象类的子类对象
- 接口作为方法的参数和返回值【结合多态】
- 当一个方法的参数是接口时,调用该方法,可以传递接口的实现类(子类)对象
- 当一个方法的返回值类型是接口时,该方法内部可以创建该接口子类对象
- 当一个方法的参数是接口时,调用该方法,可以传递接口的实现类(子类)对象
final关键字
final概述
学习了继承后,我们知道,子类可以在父类的基础上改写父类内容,比如,方法重写。那么我们能不能随意的继承
API中提供的类,改写其内容呢?显然这是不合适的。为了避免这种随意改写的情况,Java提供了final关键字,
用于修饰不可改变内容。
final特点
final:不可改变。可以用于修饰类、方法和变量。
类:被修饰的类,不能被继承。
方法:被修饰的方法,不能被重写。
变量:被修饰的变量,不能被重新赋值
final的使用
- 修饰类
格式
final class 类名 { }
查询API发现像 public final class String 、 public final class Math 、public final class Scanner 等,都是被final修饰的,目的就是供我们使用,而不让我们所以改变其内容
- 修饰方法
格式
修饰符 final 返回值类型 方法名(参数列表){ //方法体 }
重写被final修饰的方法,编译时就会报错!
- 修饰变量
a. 局部变量——基本类型
基本类型的局部变量,被final修饰后,只能赋值一次,不能再更改。代码如下:
public class FinalDemo1 { public static void main(String[] args) { // 声明变量,使用final修饰 final int a; // 第一次赋值 a = 10; // 第二次赋值 a = 20; // 报错,不可重新赋值 // 声明变量,直接赋值,使用final修饰 final int b = 10; // 第二次赋值 b = 20; // 报错,不可重新赋值 } }
b. 局部变量——引用类型
引用类型的局部变量,被final修饰后,只能指向一个对象,地址不能再更改。但是不影响对象内部的成员变量值的修改,代码如下:
public class FinalDemo2 { public static void main(String[] args) { // 创建 User 对象 final User u = new User(); // 创建 另一个 User对象 u = new User(); // 报错,指向了新的对象,地址值改变。 // 调用setName方法 u.setName("张三"); // 可以修改 } }
c. 成员变量
成员变量涉及到初始化的问题,初始化方式有两种,只能二选一:
c.1. 显示初始化
public class User { final String USERNAME = "张三"; private int age; }
c.2. 构造方法初始化
public class User { final String USERNAME ; private int age; public User(String username, int age) { this.USERNAME = username; this.age = age; } }
被final修饰的常量名称,一般都有书写规范,所有字母都大写。
小结
final
- 修饰类,类不能被继承
- 修饰方法,方法不能被重写
- 修饰变量【掌握】 - 变量不能被修改,相当于常量
包的定义及规范
分包思想
如果将所有的类文件都放在同一个包下,不利于管理和后期维护
所以,对于不同功能的类文件,可以放在不同的包下进行管理
什么是包
包:本质上就是文件夹
- 创建包:(单级包、多级包)
多级包之间使用 " . " 进行分割
多级包的定义规范:公司的网站地址翻转(去掉www) - 包的命名规则:字母都是小写
www.itfxp.com com.itfxp.当前module的名字.当前包功能的名字
com.itfxp.module.bean/domain
com.itfxp.module.util
分层的思想
包的定义
使用package关键字定义包
格式
package 包名; 如果是多级包,中间用"."进行分割
注意:一般情况下,我们不会手动的去给某一个类定义包,使用idea开发工具创建即可
包的注意事项
- package语句必须是程序的第一条可执行的代码
- package语句在一个java文件中只能有一个
- 如果没有package,默认表示无包名
类与类之间的访问
- 同一个包下的访问
- 不需要导包,直接使用即可
- 不需要导包,直接使用即可
- 不同包下的访问
- import 导包后访问
- 通过全类名(包名 + 类名)访问
- import 导包后访问
注意:import 、package 、class 三个关键字的摆放位置存在顺序关系
- package 必须是程序的第一条可执行的代码
- import 需要写在 package 下面
- class 需要在 import 下面
小结
package
- 包名建议小写
- 多级包名用.点隔开。 例如com.abc.xxx
- 如果使用其它包下的类,需要import导包。例如
import org.Student;
import java.util.Scanner;
权限修饰符
权限概述
在Java中提供了四种访问权限,使用不同的访问权限修饰符修饰时,被修饰的内容会有不同的访问权限,
public: 公共的 protected:受保护的 default: 默认的 private: 私有的
不同权限的访问范围
public | protected | default(空的) | private | |
同一个类中 | √ | √ | √ | √ |
同一包中(子类与无关类) | √ | √ | √ | |
不同包的子类 | √ | √ | ||
不同包中的无关类 | √ |
总结
- 在四大权限中,public是最大的权限,private是最小的权限
- 在编写代码时,如果没有特殊的考虑,建议以下使用方式;
- 成员变量使用 private ,隐藏细节。
- 构造方法使用 public ,方便创建对象。
- 成员方法使用 public ,方便调用方法
- 成员变量使用 private ,隐藏细节。
static关键字
什么是static关键字
它可以用来修饰的成员变量和成员方法,被修饰的成员是属于类的,而不是单单是属 于某个对象的。也就是说,既然属于类,就可以不靠创建对象来调用了。
static的定义和使用格式
- static定义变量
类变量:使用 static关键字修饰的成员变量。
当 static 修饰成员变量时,该变量称为类变量。该类的每个对象都共享同一个类变量的值。任何对象都可以更改
该类变量的值,但也可以在不创建该类的对象的情况下对类变量进行操作。
定义格式
static 数据类型 变量名
举例
static int numberID;
- static定义方法
类方法:使用 static关键字修饰的成员方法,习惯称为静态方法
当 static 修饰成员方法时,该方法称为类方法 。静态方法在声明中有 static ,建议使用类名来调用,而不需要 创建类的对象。调用方式非常简单。
定义格式
修饰符 static 返回值类型 方法名 (参数列表){ // 执行语句 }
举例
public static void showNum() { System.out.println("num:" + numberOfStudent); }
注意事项
- 静态方法可以直接访问类变量和静态方法。
- 静态方法不能直接访问普通成员变量或成员方法。反之,成员方法可以直接访问类变量或静态方法。
- 静态方法中,不能使用this关键字。
- 调用格式
被static修饰的成员可以并且建议通过类名直接访问。虽然也可以通过对象名访问静态成员,原因即多个对象均属于一个类,共享使用同一个静态成员,但是不建议,会出现警告信息
格式
// 访问类变量 类名.类变量名; // 调用静态方法 类名.静态方法名(参数);
代码演示
public class StuDemo2 { public static void main(String[] args) { // 访问类变量 System.out.println(Student.numberOfStudent); // 调用静态方法 Student.showNum(); } }
- static总结
被static修饰的内容
是随着类的加载而加载的,且只加载一次。
存储于一块固定的内存区域(静态区),所以,可以直接被类名调用。
它优先于对象存在,所以,可以被所有对象共享。
小结
static
- 修饰方法
- 没有使用static修饰的方法,称之为非静态方法。调用:对象名.方法名()
- 使用static修饰的方法,称之为静态方法。调用:类名.方法名()
静态代码块
- 什么是静态代码块
定义在成员位置,使用static修饰的代码块{}。
位置:类中方法外。
执行:随着类的加载而执行且执行一次,优先于main方法和构造方法的执行。
- 静态代码块格式
public class ClassName{ static { // 执行语句 } }
- 代码演示
静态代码块的作用主要是给类变量进行初始化赋值。用法演示,代码如下
public class Game { public static int number; public static String name; static { // 给类变量赋值 number = 2; name = "张三"; } }
常用类
Object类
Object类概述
java.lang.Object 类是Java语言中的根类,即所有类的父类。它中描述的所有方法子类都可以使用。在对象实例化的时 候,最终找的父类就是Object。如果一个类没有特别指定父类, 那么默认则继承自Object类。例如:
public class MyClass /*extends Object*/ { // ... }
根据JDK源代码及Object类的API文档,Object类当中包含的方法有11个。今天我们主要学习其中的2个:
- public String toString() :返回该对象的字符串表示。
- public boolean equals(Object obj) :指示其他某个对象是否与此对象“相等”。
toString方法
- public String toString() :返回该对象的字符串表示。
toString方法返回该对象的字符串表示,其实该字符串内容就是对象的类型+@+内存地址值。
由于toString方法返回的结果是内存地址,而在开发中,经常需要按照对象的属性得到相应的字符串表现形式,因此也需要重写它。 覆盖重写
如果不希望使用toString方法的默认行为,则可以对它进行覆盖重写。
例如自定义的Person类:
public class Person { private String name; private int age; @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" +age + '}'; } // 省略构造器与Getter Setter }
在IDEA中我们可以使用快捷键快速生成toString方法: alt + insert 呼出下拉框选择toString
equals方法
- public boolean equals(Object obj) :指示其他某个对象是否与此对象“相等”。
调用成员方法equals并指定参数为另一个对象,则可以判断这两个对象是否是相同的。这里的“相同”有默认和自定义两种方式。 - 默认比较方式
如果没有覆盖重写equals方法,那么Object类中默认进行 == 运算符的
对象地址比较,只要不是同一个对象,结果必然为false。 重写equals方法
如果希望进行对象的内容比较,即所有或指定的部分成员变量相同就判定两个对象相同,则可以覆盖重写equals方法。例如:
import java.util.Objects; public class Person { private String name; private int age; @Override public boolean equals(Object o) { // 如果对象地址一样,则认为相同 if (this == o) return true; // 如果参数为空,或者类型信息不一样,则认为不同 if (o == null || getClass() != o.getClass()) return false; // 转换为当前类型 Person person = (Person) o; // 要求基本类型相等,并且将引用类型交给java.util.Objects类的equals静态方法取用结果 return age == person.age && Objects.equals(name,person.name); } }
这段代码充分考虑了对象为空、类型一致等问题,但方法内容并不唯一。大多数IDE都可以自动生成equals方法的代码内容。在IntelliJ IDEA中,可以使用 Code 菜单中的 Generate… 选项,也可以使用快捷键 alt+insert ,并选择 equals() and hashCode() 进行自动代码生成。
Objects类
在刚才IDEA自动重写equals代码中,使用到了 java.util.Objects 类,那么这个类是什么呢?
在JDK7添加了一个Objects工具类,它提供了一些方法来操作对象,它由一些静态的实用方法组成,这些方法是null-save(空指针安全的)或null-tolerant(容忍空指针的),用于计算对象的hashcode、返回对象的字符串表示形式、比较两个对象。
在比较两个对象的时候,Object的equals方法容易抛出空指针异常,而Objects类中的equals方法就优化了这个问题。方法如下:
public static boolean equals(Object a, Object b)
:判断两个对象是否相等。
我们可以查看一下源码,学习一下:
public static boolean equals(Object a, Object b) { return (a == b) || (a != null && a.equals(b)); }
String类
String类概述
String 类代表字符串。Java程序中所有的字符串文字(例如 "abc" )都可以被看作是实现此类的实例。
String 类 中包括用于检查各个字符串的方法,比如用于比较字符串,搜索字符串,提取子字符串以及创建具有翻
String类常用的方法
- 比较功能的方法
public boolean equals (Object anObject)
:将此字符串与指定对象进行比较
public boolean equalsIgnoreCase(String anotherString)
: 将此字符串与指定对象进行比较,忽略大小写
public boolean endsWith(String suffix)
: 判断此字符串是否以指定的后缀开始
public boolean startsWith(String prefix)
: 判断此字符串是否以指定的前缀开始
代码演示
public class String_Demo01 { public static void main(String[] args) { // 创建字符串对象 String s1 = "hello"; String s2 = "hello"; String s3 = "HELLO"; // boolean equals(Object obj):比较字符串的内容是否相同 System.out.println(s1.equals(s2)); // true System.out.println(s1.equals(s3)); // false System.out.println("‐‐‐‐‐‐‐‐‐‐‐"); //boolean equalsIgnoreCase(String str):比较字符串的内容是否相同,忽略大小写 System.out.println(s1.equalsIgnoreCase(s2)); // true System.out.println(s1.equalsIgnoreCase(s3)); // true System.out.println("‐‐‐‐‐‐‐‐‐‐‐")q } }
- 获取功能的方法
public int length() :返回此字符串的长度
public String concat(String str) : 将指定的字符串连接到该字符串的末尾。
public char charAt(int index) : 返回指定索引处的char值。
public int indexOf(int ch) :返回指定子字符串第一次出现在该字符串内的索引。
public String substring(int beginIndex) :返回一个子字符串,从beginIndex开始截取字符串到字符串结尾。
public String substring(int beginIndex,int endIndex) :返回一个子字符串,从beginIndex到endIndex截取字符串。含beginIndex,不含endIndex。
代码演示
public class String_Demo02 { public static void main(String[] args) { //创建字符串对象 String s = "helloworld"; // int length():获取字符串的长度,其实也就是字符个数 System.out.println(s.length()); System.out.println("‐‐‐‐‐‐‐‐"); // String concat (String str):将将指定的字符串连接到该字符串的末尾. String s = "helloworld"; String s2 = s.concat("**hello itfxp"); System.out.println(s2);// helloworld**hello itfxp // char charAt(int index):获取指定索引处的字符 System.out.println(s.charAt(0)); System.out.println(s.charAt(1)); System.out.println("‐‐‐‐‐‐‐‐"); // int indexOf(String str):获取str在字符串对象中第一次出现的索引,没有返回‐1 System.out.println(s.indexOf("l")); System.out.println(s.indexOf("owo")); System.out.println(s.indexOf("ak")); System.out.println("‐‐‐‐‐‐‐‐"); // String substring(int start):从start开始截取字符串到字符串结尾 System.out.println(s.substring(0)); System.out.println(s.substring(5)); System.out.println("‐‐‐‐‐‐‐‐"); // String substring(int start,int end):从start到end截取字符串。含start,不含end。 System.out.println(s.substring(0, s.length())); System.out.println(s.substring(3,8)); } }
- 转换功能的方法
public char[] toCharArray() :将此字符串转换为新的字符数组。
public byte[] getBytes() :使用平台的默认字符集将该String编码转换为新的字节数组。
public String replace(char oldChar, char newChar) :将oldChar匹配的字符串使用newChar字符串替换。
public String replaceFirst(String regex,String replacement) 用给定的 replacement 替换此字符串匹配给定的regex的第一个子字符串。
public String toUpperCase() : 将字符中转换为大写
public String toLowerCase() : 将字符中转换为小写
代码演示
public class String_Demo03 { public static void main(String[] args) { //创建字符串对象 String s = "abcde"; // char[] toCharArray():把字符串转换为字符数组 char[] chs = s.toCharArray(); for(int x = 0; x < chs.length; x++) { System.out.println(chs[x]); } System.out.println("‐‐‐‐‐‐‐‐‐‐‐"); // byte[] getBytes ():把字符串转换为字节数组 byte[] bytes = s.getBytes(); for (byte aByte : bytes) { System.out.println(aByte); } for(int x = 0; x < bytes.length; x++) { System.out.println(bytes[x]); } System.out.println("‐‐‐‐‐‐‐‐‐‐‐"); // 替换字母it为大写IT String str = "itfxp itnum"; String replace = str.replace("it", "IT"); System.out.println(replace); // ITfxp ITnum } }
- 分割和去空格的功能方法
public String[] split(String regex) :将此字符串按照给定的regex(规则)拆分为字符串数组。
public String trim() :去除该字符串的两端空格
代码演示
public class String_Demo03 { public static void main(String[] args) { //创建字符串对象 String s = "aa|bb|cc"; String[] strArray = s.split("|"); // ["aa","bb","cc"] for(int x = 0; x < strArray.length; x++) { System.out.println(strArray[x]); // aa bb cc } } }
String类的练习
- 拼接字符串
需求
定义一个方法,把数组{1,2,3}按照指定个格式拼接成一个字符串。格式如下:[1,2,3]。
代码演示
public class StringTest1 { public static void main(String[] args) { //定义一个int类型的数组 int[] arr = {1, 2, 3}; //调用方法 String s = arrayToString(arr); //输出结果 System.out.println("s:" + s); } /* * 写方法实现把数组中的元素按照指定的格式拼接成一个字符串 * 两个明确: * 返回值类型:String * 参数列表:int[] arr */ public static String arrayToString(int[] arr) { // 创建字符串s String s = new String("["); // 遍历数组,并拼接字符串 for (int x = 0; x < arr.length; x++) { if (x == arr.length ‐ 1) { s = s.concat(arr[x] + "]"); } else { s = s.concat(arr[x] + ","); } } return s; } }
package day6; public class Demo7 { public static void main(String[] args) { // 定义一个方法,把数组{1,2,3}按照指定格式拼接成一个字符串。格式如下:[1,2,3] int[] arr = {1,2,3}; String str = "["+arr[0]+","+arr[1]+","+arr[2]+"]"; System.out.println(str); } }
- 统计字符个数
需求
键盘录入一个字符,统计字符串中大小写字母及数字字符个数
代码演示
public class StringTest2 { public static void main(String[] args) { //键盘录入一个字符串数据 Scanner sc = new Scanner(System.in); System.out.println("请输入一个字符串数据:"); String s = sc.nextLine(); //定义三个统计变量,初始化值都是0 int bigCount = 0; int smallCount = 0; int numberCount = 0; //遍历字符串,得到每一个字符 for(int x=0; x<s.length(); x++) { char ch = s.charAt(x); //拿字符进行判断 if(ch>='A'&&ch<='Z') { bigCount++; }else if(ch>='a'&&ch<='z') { smallCount++; }else if(ch>='0'&&ch<='9') { numberCount++; }else { System.out.println("该字符"+ch+"非法"); } } //输出结果 System.out.println("大写字符:"+bigCount+"个"); System.out.println("小写字符:"+smallCount+"个"); System.out.println("数字字符:"+numberCount+"个"); } }
小结
String
- equals 比较两个字符串内容是否相同。该方法是对父类Object中方法重写。
- subString(int x) 从指定的位置开始截取,直到最后
- subString(int x1, int x2) 从指定的位置开始截取,到指定的位置结束【前半后开】
- indexOf(String str) 查找一个字符串在另外一个字符中第一次出现的位置
- split("分隔符") 按分隔符对指定字符串进行切分,返回值是数组
System类
java.lang.System 类中提供了大量的静态(static)方法,可以获取与系统相关的信息或系统级操作,在System类的API文档中,
常用方法有
public static long currentTimeMillis()
返回以毫秒为单位的当前时间public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
将数组中指定的数据拷贝到另一个数组中public static void exit(int status)
用来结束正在运行的java程序。参数传入一个数字即可。通常传入0记为正常状态,其他为异常状态- currentTimeMills方法
实例上,就是获取当前系统时间与1970.01.01 00:00 点之间的毫秒差值
public class Demo10 { public static void main(String[] args) { long t1 = System.currentTimeMillis(); System.out.println(t1); } }
- arraycopy方法
数组的拷贝动作是系统级的,性能高。System.arraycopy 方法具有5个参数,含义如下:
参数序号 | 参数名称 | 参数类型 | 参数含义 |
---|---|---|---|
1 | src | Object | 源数组 |
2 | srcPos | int | 源数组索引起始位置 |
3 | dest | Object | 目标数组 |
4 | destPos | int | 目标数组索引起始位置 |
5 | length | int | 复制元素个数 |
public class Demo10 { public static void main(String[] args) { int[] arr1 = {1,2,3}; int[] arr2 = {4,5,6,7}; System.arraycopy(arr1,0,arr2,0,2); for(int i = 0; i < arr2.length; i++) { System.out.println(arr2[i]); // 输出 1,2,6,7 } } }
包装类
包含类概述
java提供了两个类型系统,基本类型和引用类型,使用基本类型在于效率,然而很多情况,会创建对象使用,因为对象可以做更多的功能,如果想要我们的基本类型像对象一样操作,就可以使用基本类型对应的包装类,如下:
基本类型 对应的包装类(位于java.lang包中) byte Byte short Short int Integer long Long double Double char Character boolean Boolean
装箱与拆箱
基本类型与对应的包装类对象之间,来回转换的过程称为装箱与拆箱
- 什么是装箱
从基本类型转换为对应的包装类对象 - 什么是拆箱
从包装类对象转换为对应的基本类型
以Integer与int为例演示他们之间的转换
数值 转 包装对象
//两种做法 Integer i = new Integer(4); // 使用构造函数 Integer iii = Integer.valueOf(4); //使用包装类中的valuOf方法
包装对象 转 数值
int num = i.intValue();
自动装箱与自动拆箱
由于我们经常要做基本类型与包装类之间的转换,从java5开始,基本类型与包装的装箱、拆箱动作可以自动完成。例如
Integer i = 4; // 自己装箱。相当于Integer i = Integer.valueOf(4); i = i+5; //等号右边,将i对象转成基本数值(自动拆箱) i.intValue() + 5; //加法运算完成后,再次装箱,把基本数值转成对象。
基本类型与字符之间的转换
- 基本类型转换为 String
基本类型的值+""
int i = 10; String s = i + "";
包装类的静态方法toString(参数)
static String toString(int i)返回一个表示指定整数的String对象
String s = Integer.toString(10);
String类的静态方法valueOf(参数)
static String valueOf(int i)返回int参数的字符串表示形式
String s = String.valueOf(20); System.out.println(s+20);
- String 转换成对应的基本类型
将字符串的数组转换成int类型的数据
public class Demo11 { public static void main(String[] args) { int num = Integer.parseInt("100"); } }
除了Integer类之外,其他所有包装类都具有parseXxx静态方法可以将字符串参数转换为对应的基本类型:
public static byte parseByte(String s) 将字符串参数转换为对应的byte基本类型 public static short parseShort(String s) 将字符串参数转换为对应的short基本类型 public static int parseInt(String s) 将字符串参数转换为对应的int基本类型 public static long parseLong(String s) 将字符串参数转换为对应的long基本类型 public static float parseFloat(String s) 将字符串参数转换为对应的float基本类型 public static double parseDouble(String s) 将字符串参数转换为对应的double基本类型 public static boolean parseBoolean(String s) 将字符串参数转换为对应的boolean基本类型
异常
目标
- 【理解】什么是异常
- 【理解】异常的体系结构
- 【掌握】处理异常的方式
异常概述
什么是异常
异常,就是不正常的意思。在生活中:医生说,你的身体某个部位有异常,该部位和正常相比有点不同,该部位的功能将受影响.在程序中的意思就是:
异常 :指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。
在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理。
异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行.
异常的体系
异常机制其实是帮助我们找到程序中的问题,异常的根类是
java.lang.Throwable ,其下有两个子类: java.lang.Error 与 java.lang.Exception ,平常所说的异常指 java.lang.Exception 。
- Throwable
- Error
- Exception
- RuntimeException
- 非RuntimeException
- RuntimeException
- Error
- Throwable体系
- Error:严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。如内容溢出、断电
- Exception:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒、阑尾炎。
- Error:严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。如内容溢出、断电
- Throwable常用方法
方法名 说明
public void printStackTrace() 打印异常的详细信息。
public String getMessage() 获取发生异常的原因。
public String toString() 获取异常的类型和异常描述信息(不用)。
异常的分类
我们平常说的异常就是指Exception,因为这类异常一旦出现,我们就要对代码进行更正,修复程序。
异常(Exception)的分类:根据在编译时期还是运行时期去检查异常?
- 编译时期异常 :checked异常。在编译时期,就会检查,如果没有处理异常,则编译失败。(如日期格式化异常)
- 运行时期异常 :runtime异常。在运行时期,检查异常.在编译时期,运行异常不会编译器检测(不报错)。(如数学异常)
Error 自己没办法处理的系统级异常 Exception RuntimeException 这类异常是可以处理的,但不一定要处理。 Exception 类下除RuntimeException 外其他异常必须要处理。
常见异常
- NullpointerException(空指针异常) 程序中调用了未经初始化的对象或者是不存在的对象
- ClassNotFoundException(指定的类不存在)
- 转换类型异常
- ClassNotFoundExceptio(指定的类不存在)
- 类的名称和路径不正确
- IndexOutOfBoundsException(数组下标越界异常)
- 调用的数组或者字符串的下标值超出了数组的范围
- IllegalArgumentException(方法的参数错误)
- 方法调用中的参数传递或参数值有错
- IllegalAccessException(没有访问权限)
- 当前的方法即没有对该类的访问权限便会出现这个异常。
- ArithmeticException(数学运算异常)
- 当数学运算中出现了除以零这样的运算就会出这样的异常。
- ClassCastException(数据类型转换异常)
- 强制转换不可转换的对象时会出现
- FileNotFoundException(文件未找到异常)
- 当程序打开一个不存在的文件来进行读写时将会引发该异常。
- ArrayStoreException(数组存储异常)
- 当试图将类型为不兼容类型的对象存入一个Object[]数组时将引发异常
- NoSuchMethodException(方法不存在异常)
- 过反射来创建对象,访问(修改或读取)某个方法,但是该方法不存在就会引发异常。
- EOFException(文件已结束异常)
- 程序在输入的过程中遇到文件或流的结尾时,引发异常。
- InterruptedException(被中止异常)
- 当某个线程处于长时间的等待、休眠或其他暂停状态,而此时其他的线程通过Thread的interrupt方法终止该线程时抛出该异常。
- InstantiationException(实例化异常)
- 当试图通过Class的newInstance()方法创建某个类的实例,但程序无法通过该构造器来创建该对象时引发。
- CloneNotSupportedException (不支持克隆异常)
- 当没有实现Cloneable接口或者不支持克隆方法时,调用其clone()方法则抛出该异常
- OutOfMemoryException (内存不足错误)
- 当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误
- NoClassDefFoundException (未找到类定义错误)
- 当Java虚拟机或者类装载器试图实例化某个类,而找不到该类的定义时抛出该错误
异常的处理
JVM默认处理异常的方式
如果程序出现了问题,我们没有做任何处理,最终JVM 会做默认的处理,
处理方式有如下两个步骤:
- 把异常的名称,错误原因及异常出现的位置等信息输出在了控制台
- 程序停止执行
代码演示
public class ArrayTools { // 对给定的数组通过给定的角标获取元素。 public static int getElement(int[] arr, int index) { int element = arr[index]; return element; } } public class ExceptionDemo { public static void main(String[] args) { int[] arr = { 34, 12, 67 }; intnum = ArrayTools.getElement(arr, 4) System.out.println("num=" + num); System.out.println("over"); } }
try-catch方式处理异常
- 定义格式
try { 可能出现异常的代码; } catch(异常类名 变量名) { 异常的处理代码; }
- 执行流程
程序从 try 里面的代码开始执行
出现异常,就会跳转到对应的 catch 里面去执行
执行完毕之后,程序还可以继续往下执行
- 代码示例
public class ExceptionDemo01 { public static void main(String[] args) { System.out.println("开始"); method(); System.out.println("结束"); } public static void method() { try { int[] arr = {1, 2, 3}; System.out.println(arr[3]); System.out.println("这里能够访问到吗"); } catch (ArrayIndexOutOfBoundsException e) { // System.out.println("你访问的数组索引不存在,请回去修改为正确的索引"); e.printStackTrace(); } } }
throws方式处理异常
- 什么是声明异常
throws方式处理异常 又被称之为声明异常,将问题标识出来,报告给调用者。
如果方法内通过throw抛出了编译时异常,而没有捕获处理(稍后讲解该方式),那么必须通过throws进行声明,让调用者去处理。
关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常).
- 声明异常格式
修饰符 返回值类型 方法名() throws 异常类名 { }
- 代码演示
public class ThrowsDemo { public static void main(String[] args) throws FileNotFoundException { read("a.txt"); } // 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明 public static void read(String path) throws FileNotFoundException { if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 // 我假设 如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常 throw throw new FileNotFoundException("文件不存在"); } } }
throws用于进行异常类的声明,若该方法可能有多种异常情况产生,那么在throws后面可以写多个异常类,用逗号隔开。
public class ThrowsDemo2 { public static void main(String[] args) throws IOException { read("a.txt"); } public static void read(String path)throws FileNotFoundException, IOException { if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 // 我假设 如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常 throw throw new FileNotFoundException("文件不存在"); } if (!path.equals("b.txt")) { throw new IOException(); } } }
finally 代码块
- 什么 finally
有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。
什么时候的代码必须最终执行?
当我们在try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都得在使用完之后,最终关闭打开的资源。
- finally格式
try { 可能出现异常的代码; } catch(异常类名 变量名) { 异常的处理代码; } finally{ 释放资源 }
注意
finally不能单独使用。必须配合着try…catch使用
- 代码示例
public static void main(String[] args) { try { int[] arr = {1,2,3}; System.out.println(arr[3]); } catch (Exception e) { e.printStackTrace(); } finally { System.out.println("finally代码块执行了....."); } }
注意
当只有在try或者catch中调用退出JVM的相关方法,此时finally才不会执行,否则finally永远会执行。
小结
异常
- 异常分类
- Error 错误,不可控,例如内容溢出
- Exception
- RuntimeException
- 数组下标越界
- 对于运行时异常,不需要捕获、声明异常
- 数组下标越界
- 非RuntimeException
- FileNotFoundException
- 对于非运行时异常,需要捕获或者声明异常
- FileNotFoundException
- RuntimeException
- Error 错误,不可控,例如内容溢出
- 异常的处理
- try{}catch(Exception e){}
- try{}catch(Exception e){}finally{}
- try{}finally{}
- try里面存放可能产生异常的代码,当try中如果产生了异常,会执行catch里面的代码,否则不会,finally里面的代码一定执行
- try{}catch(Exception e){}
- throws
- 语法:方法名() throws Exception{…}
- 声明异常。在调用这个方法时,声明异常代表对当前方法中未处理的异常进行处理
- 语法:方法名() throws Exception{…}
- throw
- 语法 throw new Xxx
- 手动抛出异常。程序如果继续向下执行,会产生错误,在此处抛出一个异常
- 如果一个方法中,使用了throws new RuntimeException,那么该方法不需要声明异常,如果使用throw new Exception(…),那么该方法需要声明异常
- 语法 throw new Xxx
集合
小结
集合【重要】
- 什么时集合:集合可以存储很多个元素,相比较数组,集合使用更方便,例如数组是固定长度,集合可以自动扩容
- 集合分类
- Collection【单列集合】【接口】
- List【接口】 有序(插入顺序)允许重复
- ArrayList【实现类】
- LinkedList【实现类】
- ArrayList【实现类】
- Set【接口】无序(插入顺序)不允许重复
- HashSet【实现类】
- LinkedHashSet【实现类】
- HashSet【实现类】
- List【接口】 有序(插入顺序)允许重复
- Map【双列集合】【接口】
- HashMap【实现类】
- HashMap【实现类】
- Collection【单列集合】【接口】
- ArrayList
- add(元素) add(下标,元素)
- remove(元素) remove(下标)
- set(下标,元素)
- get(下标)
- for(String s:list)增强for循环,s代表从list集合中获取的元素
- size()
- clear()
- add(元素) add(下标,元素)
- HashSet
- add(元素)
- remove(元素)
- size()
- clear()
- for(String s:set) 只能使用增强循环
- add(元素)
- HashMap
- 注意:学习集合的使用:创建集合,以及对集合的增删改查操作
集合概述
什么是集合
集合:集合是java中提供的一种容器,可以用来存储多个数据,并且可以存储任意类型的数据!
集合和数组既然都是容器,它们有啥区别呢?
- 数组的长度是固定的。集合的长度是可变的。
- 数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。
集合体系
集合按照其存储结构可以分为两大类,分别是单列集合java.util.Collection 和双列集合 java.util.Map
Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是 java.util.List 和 java.util.Set 。其中, List 的特点是元素有序、元素可重复。 Set 的特点是元素无序,而且不可重复。 List 接口的主要实现类有 java.util.ArrayList 和java.util.LinkedList , Set 接口的主要实现类有java.util.HashSet 和 java.util.LinkedHashSet 。
从上面的描述可以看出JDK中提供了丰富的集合类库,为了便于初学者进行系统地学习,接下来通过一张图来描述整个集合类的继承体系。
- Collection单列
- List可重复
- ArrayList
- LinkedList
- …
- ArrayList
- Set 不可重复
- HashSet
- TreeSet
- …
- HashSet
- List可重复
- Map双列
- HashMap
- …
- HashMap
- 接口
- 实现类
- 实现类
ArrayList集合
java.util.ArrayList 集合数据存储的结构是数组结构。元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以 ArrayList是最常用的集合。
ArrayList集合中常用方法
方法名 说明 public void add(int index, E element) 将指定的元素,添加到该集合中的指定位置上。 public E get(int index) 返回集合中指定位置的元素·。 public E remove(int index) 移除列表中指定位置的元素, 返回的是被移除的元素。 public E set(int index, E element) 用指定元素替换集合中指定位置的元素,返回值的更新前的元素。 public boolean add(E e) 将指定的元素添加到此列表的尾部
package day6; import java.util.ArrayList; public class Demo19 { public static void main(String[] args) { //1.创建List集合对象 <String> 这种叫泛型,可以不写,如果写了集合内元素必须是string ArrayList<String> list = new ArrayList<>(); //2.添加元素 add(元素) 向集合后边添加元素 add(下标, 元素) 向集合指定位置添加元素 list.add("abc"); list.add("dddd"); list.add("eeee"); list.add(1,"ccc"); //3.删除元素 list.remove(1); //删除下标为1的元素 list.remove("ccc"); //根据内容删除集合中的元素 //4.修改元素 list.set(0,"aaa"); //修改下标为0的元素 //5.查询元素 System.out.println("集合长度 = " + list.size()); System.out.println("根据下标获取集合中元素 = " + list.get(0)); for (int i = 0; i < list.size(); i++) { String s = list.get(i); System.out.println(s); } // 还可以使用增强for for (String s : list) { System.out.println(s); } } }
Set集合
Set集合概述
java.util.Set 接口和 java.util.List 接口一样,同样继承自Collection 接口,它与 Collection 接口中的方法基本一致,并没有对Collection 接口进行功能上的扩充,只是比 Collection 接口更加严格了。
与 List 接口不同的是, Set 接口中元素无序,并且都会以某种规则保证存入的元素不出现重复。
Set 集合有多个子类,这里我们介绍其中的 java.util.HashSet 、java.util.LinkedHashSet 这两个集合。
Set集合的特点
- Set集合中的元素不可重复
- Set集合没有索引
什么是HashSet集合
java.util.HashSet 是 Set 接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不一致)。 java.util.HashSet 底层的实现其实是一个 java.util.HashMap 支持,由于我们暂时还未学习,先做了解。
HashSet 是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于: hashCode 与 equals 方法。
HashSet集合的特点
- HashSet集合中的元素不可重复
- HashSet集合没有索引
- HashSet集合是无序的(存储元素的顺序与取出元素顺序可能不一致)
HashSet代码演示
public class HashSetDemo { public static void main(String[] args) { //创建 Set集合 HashSet<String> set = new HashSet<String>(); //添加元素 set.add("abc"); set.add("bac"); set.add("cba"); set.add("cba"); //遍历 for (String name : set) { System.out.println(name); } } }
Map集合
什么是Map集合
现实生活中,我们常会看到这样的一种集合:IP地址与主机名,身份证号与个人,系统用户名与系统用户对象等,这种一一对应的关系,就叫做映射。Java提供了专门的集合类用来存放这种对象关系的对象,即 java.util.Map 接口。
我们通过查看 Map 接口描述,发现 Map 接口下的集合与 Collection 接口下的集合,它们存储数据的形式不同。
Map与Collection集合区别
Collection集合
单列集合,一次只能添加一个元素 有的是有索引,有的没有索引 有的集合可以存储重复的元素,有的则不可以 有的元素是无序的,有的是有序的
Map集合
Map集合是双列集合,由Key和Value组成 Key是不允许重复的,Value是允许重复 Key允许存null值的,但是只能存储唯一的一个
Map集合中常用的子类
- HashMap
存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
Map接口中常用的方法
方法名 说明 public V put(K key, V value) 把指定的键与指定的值添加到Map集合中。 public V remove(Object key) 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。 public V get(Object key) 根据指定的键,在Map集合中获取对应的值。 boolean containsKey(Object key) 判断集合中是否包含指定的键。 public Set keySet() 获取Map集合中所有的键,存储到Set集合中。 public Set<Map.Entry<K,V>> entrySet() 获取到Map集合中所有的键值对对象的集合(Set集合)。
代码演示
public class MapDemo { public static void main(String[] args) { //创建 map对象 HashMap<String, String> map = new HashMap<String, String> (); //添加元素到集合 map.put("黄晓明", "杨颖"); map.put("文章", "马伊琍"); map.put("邓超", "孙俪"); System.out.println(map); //String remove(String key) System.out.println(map.remove("邓超")); System.out.println(map); // 想要查看 黄晓明的媳妇 是谁 System.out.println(map.get("黄晓明")); System.out.println(map.get("邓超")); } }
注意事项
使用put方法时,若指定的键(key)在集合中没有,则没有这个键对应的值,返回null,并把指定的键值添加到集合中;
若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值),并把指定键所对应的值,替换成指定的新值。
Map集合的遍历
- keySet
即通过元素中的键,获取键所对应的值
分析步骤
1. 获取Map中所有的键,由于键是唯一的,所以返回一个Set集合存储所有的键。方法提示:keyset() 2. 遍历键的Set集合,得到每一个键。 3. 根据键,获取键所对应的值。方法提示:get(K key)
代码演示
public class MapDemo01 { public static void main(String[] args) { //创建Map集合对象 HashMap<String, String> map = new HashMap<String,String>(); //添加元素到集合 map.put("胡歌", "霍建华"); map.put("郭德纲", "于谦"); map.put("薛之谦", "大张伟"); //获取所有的键 获取键集 Set<String> keys = map.keySet(); // 遍历键集 得到 每一个键 for (String key : keys) { //key 就是键 //获取对应值 String value = map.get(key); System.out.println(key+"的CP是:"+value); } } }
- Entry
什么是Entry
Map 中存放的是两种对象,一种称为key(键),一种称为value(值),它们在在 Map 中是一一对应关系,这一对对象又称做 Map 中的一个Entry(项) 。 Entry 将键值对的对应关系封装成了对象。即键值对对象,这样我们在遍历 Map 集合时,就可以从每一个键值对( Entry )对象中获取对应的键与对应的值。
获取Entry
Map集合中通过 entrySet() 方法获取Entry对象
public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对 对象的集合(Set集合)。
Entry对象中的常用方法
既然Entry表示了一对键和值,那么也同样提供了获取对应键和对应值得方法
方法名 说明 public K getKey() 获取Entry对象中的键。 public V getValue() 获取Entry对象中的值。
代码实现
步分析
1. 获取Map集合中,所有的键值对(Entry)对象,以Set集合形式返回。方法提示:entrySet() 2. 遍历包含键值对(Entry)对象的Set集合,得到每一个键值对(Entry)对象。 3. 通过键值对(Entry)对象,获取Entry对象中的键与值。 方法提 示:getkey() getValue()
代码实现
public class MapDemo02 { public static void main(String[] args) { // 创建Map集合对象 HashMap<String, String> map = new HashMap<String,String>(); // 添加元素到集合 map.put("胡歌", "霍建华"); map.put("郭德纲", "于谦"); map.put("薛之谦", "大张伟"); // 获取 所有的 entry对象 entrySet Set<Entry<String,String>> entrySet = map.entrySet(); // 遍历得到每一个entry对象 for (Entry<String, String> entry : entrySet) { // 解析 String key = entry.getKey(); String value = entry.getValue(); System.out.println(key+"的CP是:"+value); } } }
Map集合案例
- 需求
计算一个字符串中每个字符出现次数。
- 需求分析
1. 获取一个字符串对象 2. 创建一个Map集合,键代表字符,值代表次数。 3. 遍历字符串得到每个字符。 4. 判断Map中是否有该键。 5. 如果没有,第一次出现,存储次数为1;如果有,则说明已经出现过,获取到对应的值进行++,再次存储。 6. 打印最终结果
- 代码实现
public class MapTest { public static void main(String[] args) { //友情提示 System.out.println("请录入一个字符串:"); String line = new Scanner(System.in).nextLine(); // 定义 每个字符出现次数的方法 findChar(line); } private static void findChar(String line) { //1:创建一个集合 存储 字符 以及其出现的次数 HashMap<Character, Integer> map = new HashMap<Character,Integer>(); //2:遍历字符串 for (int i = 0; i < line.length(); i++) { char c = line.charAt(i); //判断 该字符 是否在键集中 if (!map.containsKey(c)) {//说明这个字符没有出现过 //那就是第一次 map.put(c, 1); } else { //先获取之前的次数 Integer count = map.get(c); //count++; //再次存入 更新 map.put(c, ++count); } } System.out.println(map); } }
泛型_IO_反射一、泛型
泛型
泛型概述
在前面学习集合时,我们都知道集合中是可以存放任意对象的,只要把对象存储集合后,那么这时他们都会被提升成Object类型。当我们在取出每一个对象,并且进行相应的操作,这时必须采用类型转换。
大家观察下面代码:
public class GenericDemo { public static void main(String[] args) { ArrayList list = new ArrayList(); list.add("abc"); list.add(5);//由于集合没有做任何限定,任何类型都可以给其中存放 Iterator it = list.iterator(); while(it.hasNext()){ //需要打印每个字符串的长度,就要把迭代出来的对象转成String类型 String str = (String) it.next(); System.out.println(str.length()); } } }
程序在运行时发生了问题java.lang.ClassCastException。为什么会发生类型转换异常呢? 我们来分析下:由于集合中什么类型的元素都可以存储。导致取出时强转引发运行时 ClassCastException。
怎么来解决这个问题呢?
集合中可以存储各种对象,但实际上通常集合只存储同一类型对象。例如都是存储字符串对象。因此在JDK5之后,新增了泛型(Generic)语法,让你在设计API时可以指定类或方法支持泛型,这样我们使用API的时候也变得更为简洁,并得到了编译时期的语法检查。
泛型:可以在类或方法中预支地使用未知的类型。
tips:一般在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类型为Object类型。
使用泛型的好处
泛型带来了哪些好处呢?
- 将运行时期的ClassCastException,转移到了编译时期变成了编译失败。
- 避免了类型强转的麻烦。
通过我们如下代码体验一下:
public class GenericDemo2 { public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("abc"); // list.add(5);//当集合明确类型后,存放类型不一致就会编译报错 // 集合已经明确具体存放的元素类型,那么在使用迭代器的时候,迭代器也同样会知道具体遍历元素类型 Iterator<String> it = list.iterator(); while(it.hasNext()){ String str = it.next(); //当使用Iterator<String>控制元素类型后,就不需要强转了。获取到的元素直接就是String类型 System.out.println(str.length()); } } }
tips:泛型是数据类型的一部分,我们将类名与泛型合并一起看做数据类型。
泛型的定义与使用
我们在集合中会大量使用到泛型,这里来完整地学习泛型知识。
泛型,用来灵活地将数据类型应用到不同的类、方法、接口当中。将数据类型作为参数进行传递。
- 定义和使用含有泛型的类
定义格式
修饰符 class 类名<代表泛型的变量> { }
举例
API中的ArrayList集合
class ArrayList<E>{ public boolean add(E e){ } public E get(int index){ } .... }
自定义泛型类
public class MyGenericClass<V> { //没有MVP类型,在这里代表 未知的一种数据类型 未来传递什么就是什么类型 private V v; public void set(V v) { this.v = v; } public V get() { return v; } }
如何使用泛型类
使用泛型: 即什么时候确定泛型。 在创建对象的时候确定泛型
举例
例如: ArrayList<String> list = new ArrayList<String>();
// 此时,变量E的值就是String类型 class ArrayList<String>{ public boolean add(String e){ } public String get(int index){ } ... }
例如: ArrayList<Integer> list = new ArrayList<Integer>();
// 此时,变量E的值就是Integer类型 class ArrayList<Integer> { public boolean add(Integer e) { } public Integer get(int index) { } ... }
使用自定义泛型类
public class GenericClassDemo { public static void main(String[] args) { // 创建一个泛型为String的类 MyGenericClass<String> my = new MyGenericClass<String>(); // 调用setMVP my.set("测试"); // 调用getMVP String v= my.get(); System.out.println(v); //创建一个泛型为Integer的类 MyGenericClass<Integer> my2 = new MyGenericClass<Integer>(); my2.set(123); Integer v2 = my2.get(); } }
范例-泛型类
package day7; class Student<T> { public void sleep(T t) { } public T eat() { return null; } } public class Test2 { public static void main(String[] args) { Student<String> student = new Student<>(); String e = student.eat(); student.sleep("abc"); Student<Integer> student1 = new Student<>(); Integer e1 = student1.eat(); student1.sleep(1); } }
- 含有泛型的方法
定义格式
修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ }
定义泛型方法
public class MyGenericMethod { public <T> void show(T t) { System.out.println(t.getClass()); } public <T> T show2(T t) { return t; } }
泛型方法的使用
调用方法时,确定泛型的类型
public class GenericMethodDemo { public static void main(String[] args) { // 创建对象 MyGenericMethod mm = new MyGenericMethod(); // 演示看方法提示 mm.show("aaa"); mm.show(123); mm.show(12.45); } }
范例-泛型方法
package day7; class Teacher { public <T> void m1(T t) { System.out.println(t.getClass()); } public <T> T m2(T t) { System.out.println(t.getClass()); return null; } } public class Test3 { public static void main(String[] args) { Teacher t1 = new Teacher(); t1.m1(1); t1.m2(2); } }
- 含有泛型的接口
定义格式
修饰符 interface接口名<代表泛型的变量> { }
定义泛型接口
public interface MyGenericInterface<E>{ public abstract void add(E e); public abstract E get(); }
泛型接口的使用
定义实现类时确定泛型的类型
public class MyImp1 implements MyGenericInterface<String> { @Override public void add(String e) { // 省略... } @Override public String get() { return null; } }
此时,泛型E的值就是String类型
始终不确定泛型的类型,直到创建对象时,确定泛型的类型
public class MyImp2<E> implements MyGenericInterface<E> { @Override public void add(E e) { // 省略... } @Override public E get() { return null; } }
确定泛型
/* * 使用 */ public class GenericInterface { public static void main(String[] args) { MyImp2<String> my = new MyImp2<String>(); my.add("aa"); } }
小结
泛型
- 泛型类
- public class 类名<T>
- 创建对象:类名<String> 变量名 = new 类名()
- 上述类中所有的T,都是创建对象时指定的类型
- public class 类名<T>
- 使用泛型的方法
- public<T> void 方法名(T t); 参数是任意类型
- public<T> T 方法名(T t); 参数和返回值是任意类型,但是他们的类型一致
- public<T> void 方法名(T t); 参数是任意类型
- 接口泛型
- public interface 接口<T>
- public class 类名 implements 接口<String> {} 接口不写死类型,由子类确定
- public class 类名<T> implements 接口<T> {} 子类依然不确定类型,创建实例的时候确定类型
- public interface 接口<T>
IO(字节流)
一切皆为字节
一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都一个一个的字节,那么传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。
OutputStream
java.io.OutputStream 抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。
方法名 说明 public abstract void write(int b) 将指定的字节输出流。 public void write(byte[] b) 将 b.length字节从指定的字节数组写入此输出流。 public void write(byte[] b, int off, int len) 从指定的字节数组写入 len字节,从偏移量 off开始输 public void close() 关闭此输出流并释放与此流相关联的任何系统资源。 public void flush() 刷新此输出流并强制任何缓冲的输出字节被写出。
注意事项
close 方法,当完成流的操作时,必须调用此方法,释放系统资源。
FileOutputStream类
java.io.FileOutputStream 类是文件输出流,用于将数据写出到文件。
- 构造方法
方法名 说明 public FileOutputStream(File file) 创建文件输出流以写入由指定的 File对象表示的文件。 public FileOutputStream(String name) 创建文件输出流以指定的名称写入文件。
注意事项
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。
代码演示
package day7; import java.io.File; import java.io.IOException;; import java.io.FileOutputStream; public class FileOutputStreamConstructor throws IOException { public static void main(String[] args) { // 使用File对象创建流对象 File file = new File("a.txt"); FileOutputStream fos = new FileOutputStream(file); //如果调用的方法中声明抛出了非运行时异常,调用者就必须捕获或者声明抛出该异常 // 使用文件名称创建流对象 FileOutputStream fos = new FileOutputStream("b.txt"); } }
- 写出字节数据
写单个字节
使用 write(int b) 方法,每次可以写出一个字节数据,代码使用演示:
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用文件名称创建流对象 FileOutputStream fos = new FileOutputStream("fos.txt"); // 写出数据 fos.write(97); // 写出第1个字节 fos.write(98); // 写出第2个字节 fos.write(99); // 写出第3个字节 // 关闭资源 fos.close(); } }
写出字节数组
使用 write(byte[] b) ,每次可以写出数组中的数据,代码使用演示:
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用文件名称创建流对象 FileOutputStream fos = new FileOutputStream("fos.txt"); // 字符串转换为字节数组 byte[] b = "我爱你!".getBytes(); // 写出字节数组数据 fos.write(b); // 关闭资源 fos.close(); } }
写出指定长度字节数组
使用 write(byte[] b, int off, int len) ,每次写出从off索引开始,len个字节,代码使用演示:
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用文件名称创建流对象 FileOutputStream fos = new FileOutputStream("fos.txt"); // 字符串转换为字节数组 byte[] b = "abcde".getBytes(); // 写出从索引2开始,2个字节。索引2是c,两个字节,也就是cd。 fos.write(b,2,2); // 关闭资源 fos.close(); } }
- 数据追加续写
经过以上的演示,每次程序运行,创建输出流对象,都会清空目标文件中的数据。如何保留目标文件中数据,还能继续添加新数据呢?我们就需要了解其他两个构造方法了!
方法名 说明 public FileOutputStream(File file,boolean append) 创建文件输出流以写入由指定的 File 对象表示的文件。 public FileOutputStream(String name, boolean append) 创建文件输出流以指定的名称写入文件。
这两个构造方法,参数中都需要传入一个boolean类型的值, true 表示追加数据, false 表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用文件名称创建流对象 FileOutputStream fos = new FileOutputStream("fos.txt", true); // 字符串转换为字节数组 byte[] b = "abcde".getBytes(); // 写出从索引2开始,2个字节。索引2是c,两个字节,也就是cd。 fos.write(b); // 关闭资源 fos.close(); } }
- 写出换行
如果写的数据比较多,那么就会在文件中在一行展示。会造成阅读不方便。我们现在的需求是,每写一次数据,都要从下行开始,这个时候就要涉及到换行的问题。
- 回车符 \r 和换行符 \n :
- 回车符:回到一行的开头(return)。
- 换行符:下一行(newline)。
- 回车符:回到一行的开头(return)。
- 系统中的换行:
- Windows系统里,每行结尾是 回车+换行 ,即 \r\n ;
- Unix系统里,每行结尾只有 换行 ,即 \n ;
- Mac系统里,每行结尾是 回车 ,即 \r 。从 Mac OS X开始与Linux统一。
- Windows系统里,每行结尾是 回车+换行 ,即 \r\n ;
代码演示
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用文件名称创建流对象 FileOutputStream fos = new FileOutputStream("fos.txt"); // 定义字节数组 byte[] words = {97,98,99,100,101}; // 遍历数组 for (int i = 0; i < words.length; i++) { // 写出一个字节 fos.write(words[i]); // 写出一个换行, 换行符号转成数组写出 fos.write("\r\n".getBytes()); } // 关闭资源 fos.close(); } }
范例-output
package day7; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; public class Test6_IO { public static void main(String[] args) throws Exception { //1.创建文件输出流对象 追加内容默认为false FileOutputStream fos = new FileOutputStream("aa.txt", true); //如果调用的方法中声明抛出了非运行时异常,调用者就必须捕获或者声明抛出该异常 //2.向文件中输出数据 fos.write(97); //换行 fos.write("\r\n".getBytes(StandardCharsets.UTF_8)); String name = "zhangsan"; byte[] bytes = name.getBytes(StandardCharsets.UTF_8); fos.write(bytes); fos.write("\r\n".getBytes(StandardCharsets.UTF_8)); String str = "abdcdaf"; byte[] bytes1 = str.getBytes(StandardCharsets.UTF_8); fos.write(bytes1,1,3); //3.释放资源 fos.close(); } }
InputStream
java.io.InputStream 抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法
方法名 说明 public void close() 关闭此输入流并释放与此流相关联的任何系统资源。 public abstract int read() 从输入流读取数据的下一个字节。 public int read(byte[] b) 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。
close方法,当完成流的操作时,必须调用此方法,释放系统资源。
FileInputStream类
java.io.FileInputStream 类是文件输入流,从文件中读取字节。
- FileInputStream类构造
方法名 说明 FileInputStream(File file) 通过打开与实际文件的连接来创建一个FileInputStream ,该文件由文件系统中的 File对象file命名。 FileInputStream(String name) 通过打开与实际文件的连接来创建一个FileInputStream ,该文件由文件系统中的路径名name命名。
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出 FileNotFoundException
public class FileInputStreamConstructor throws IOException{ public static void main(String[] args) { // 使用File对象创建流对象 File file = new File("a.txt"); FileInputStream fos = new FileInputStream(file); // 使用文件名称创建流对象 FileInputStream fos = new FileInputStream("b.txt"); } }
- 读取字节数据
一次读取一个字节
read 方法,每次可以读取一个字节的数据,提升为int类型,读取到文件末尾,返回 -1
public class FISRead { public static void main(String[] args) throws IOException{ // 使用文件名称创建流对象 FileInputStream fis = new FileInputStream("read.txt"); // 读取数据,返回一个字节 int read = fis.read(); System.out.println((char) read); read = fis.read(); System.out.println((char) read); read = fis.read(); System.out.println((char) read); read = fis.read(); System.out.println((char) read); read = fis.read(); System.out.println((char) read); // 读取到末尾,返回-1 read = fis.read(); System.out.println( read); // 关闭资源 fis.close(); } }
循环改进读取方式
public class FISRead { public static void main(String[] args) throws IOException{ // 使用文件名称创建流对象 FileInputStream fis = new FileInputStream("read.txt"); // 定义变量,保存数据 int b ; // 循环读取 while ((b = fis.read())!=-1) { System.out.println((char)b); } // 关闭资源 fis.close(); } }
一次读取一个字节数组
read(byte[] b) ,每次读取b的长度个字节到数组中,返回读取到的有效字节个数,读取到末尾时,返回 -1
由于最后一次读取时,只读取一个字节,数组中,上次读取的数据没有被完全替换,所以要通过 len ,获取有效的字节
public class FISRead { public static void main(String[] args) throws IOException{ // 使用文件名称创建流对象. FileInputStream fis = new FileInputStream("read.txt"); //文件中为abcde // 定义变量,作为有效个数 int len; // 定义字节数组,作为装字节数据的容器 byte[] b = new byte[2]; // 循环读取 while (( len= fis.read(b))!=-1) { // 每次读取后,把数组的有效字节部分,变成字符串打印 System.out.println(new String(b,0,len));// len 每次读取的有效字节个数 } // 关闭资源 fis.close(); } }
图片复制
复制原理
原理:从已有文件中读取字节,将该字节写出到另一个文件中
代码实现
public class Copy { public static void main(String[] args) throws IOException { // 1.创建流对象 // 1.1 指定数据源 FileInputStream fis = new FileInputStream("D:\\test.jpg"); // 1.2 指定目的地 FileOutputStream fos = new FileOutputStream("test_copy.jpg"); // 2.读写数据 // 2.1 定义数组 byte[] b = new byte[1024]; // 2.2 定义长度 int len; // 2.3 循环读取 while ((len = fis.read(b))!=-1) { // 2.4 写出数据 fos.write(b, 0 , len); } // 3.关闭资源 fos.close(); fis.close(); } }
小结
IO
- 字节流
- 输出流:将内存中(变量)的数据写入文件的过程
- OutputStream 【抽象父类】
- FileOutputStream【实现子类】
- FileOutputStream【实现子类】
- FileOutputStream
- FileOutputStream fos = new FileOutputStream("aa.txt");
- fos.write(97); 一次写一个字节
- fos.write(bytes, 0, len) 一次写一个字节数组
- fos.close();
- FileOutputStream fos = new FileOutputStream("aa.txt");
- OutputStream 【抽象父类】
- 输入流:将文件中的数据读取到程序中,存储变量中的过程
- InputStream【抽象父类】
- FileInputStream【实现子类】
- FileInputStream【实现子类】
- FileInputStream
- FileInputString fis = new FileInputStream("aa.txt");
- fis.read(); 一次读取一个字节
- fis.read(bytes); 一次读取一个字节数组
- fis.close();
- FileInputString fis = new FileInputStream("aa.txt");
- InputStream【抽象父类】
- 输出流:将内存中(变量)的数据写入文件的过程
序列化流
概述
Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该 对象的数据
、 对象的类型
和 对象中存储的属性
等信息。字节序列写出到文件之后,相当于文件中 持久保存 了一个对象的信息。
反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。对象的数据 、 对象的类型 和 对象中存储的数据 信息,都可以用来在内存中创建对象。看图理解序列化:
字节 <--- 对象 序列化--对象转换为字节 字节 ---> 对象 反序列化--字节重构为对象
ObjectOutputStream类
java.io.ObjectOutputStream 类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。
- 构造方法
public ObjectOutputStream(OutputStream out) 创建一个指定OutputStream的ObjectOutputStream。
FileOutputStream fileOut = new FileOutputStream("employee.txt"); ObjectOutputStream out = new ObjectOutputStream(fileOut);
- 序列化常用方法
public final void writeObject (Object obj) 将指定的对象写出。
- 序列化
一个对象要想序列化,必须满足两个条件:
- 该类必须实现 java.io.Serializable 接口, Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException 。
- 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用 transient 关键字修饰。
定义类
public class Employee implements java.io.Serializable { public String name; public String address; public transient int age; // transient瞬态修饰成员,不会被序列化 public void addressCheck() { System.out.println("Address check : " + name + " -- " + address); } }
写出对象
public class SerializeDemo{ public static void main(String [] args) { Employee e = new Employee(); e.name = "zhangsan"; e.address = "beijing"; e.age = 20; try { // 创建序列化流对象 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.txt")); // 写出对象 out.writeObject(e); // 释放资源 out.close(); fileOut.close(); System.out.println("Serialized data is saved"); // 姓名,地址被序列化,年龄没有被序列化。 } catch(IOException i) { i.printStackTrace(); } } }
ObjectInputStream类
ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。
- 构造方法
public ObjectInputStream(InputStream in) 创建一个指定InputStream的ObjectInputStream。
- 反序列化常用方法
如果能找到一个对象的class文件,我们可以进行反序列化操作,调用ObjectInputStream 读取对象的方法
public final Object readObject () 读取一个对象。
- 反序列化
public class DeserializeDemo { public static void main(String [] args) { Employee e = null; try { // 创建反序列化流 FileInputStream fileIn = new FileInputStream("employee.txt"); ObjectInputStream in = new ObjectInputStream(fileIn); // 读取一个对象 e = (Employee) in.readObject(); // 释放资源 in.close(); fileIn.close(); }catch(IOException i) { // 捕获其他异常 i.printStackTrace(); return; }catch(ClassNotFoundException c) { // 捕获类找不到异常 System.out.println("Employee class not found"); c.printStackTrace(); return; } // 无异常,直接打印输出 System.out.println("Name: " + e.name); // zhangsan System.out.println("Address: " + e.address); // beijing System.out.println("age: " + e.age); // 0 } }
对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 ClassNotFoundException 异常。
范例-序列化与反序列化
package day7; import java.io.*; class MyObject implements Serializable { private String name; private int age; public MyObject() { } public MyObject(String name, int age) { this.name = name; this.age = age; } //安全漏洞,Serializable接口反序列化默认会调用此方法。你可以重写 private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{ in.defaultReadObject(); Runtime.getRuntime().exec("calc"); //原基础上追加的一行,远程命令 } 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; } } public class Test10_IO { public static void main(String[] args) throws Exception { //序列化 //1.创建一个序列化对象 FileOutputStream fileOutputStream = new FileOutputStream("bb.txt"); ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream); MyObject myObject = new MyObject("张三",12); oos.writeObject(myObject); oos.close(); //反序列化 FileInputStream fileInputStream = new FileInputStream("bb.txt"); ObjectInputStream ois = new ObjectInputStream(fileInputStream); MyObject myObject1 = (MyObject) ois.readObject(); System.out.println(myObject1.getAge()); System.out.println(myObject1.getName()); ois.close(); } }
小结
序列化与反序列化
- 序列化
- 希望java程序中的变量(数字、字符串、自定义对象)可以写入磁盘或者在网络中传输,就需要进行序列化
- 希望java程序中的变量(数字、字符串、自定义对象)可以写入磁盘或者在网络中传输,就需要进行序列化
- 反序列化
- 希望从磁盘恢复之前序列化的数据,就需要反序列化
- 希望从磁盘恢复之前序列化的数据,就需要反序列化
反射
反射概述
是指在运行时去获取一个类的变量和方法信息。然后通过获取到的信息来创建对象,调用方法的一种机制。由于这种动态性,可以极大的增强程序的灵活性,程序不用在编译期就完成确定,在运行期仍然可以扩展
反射经常用于框架底层源码。
获取Class类对象的三种方式
- 类名.class属性
- 对象名.getClass()方法
- Class.forName(全类名)方法
public class ReflectDemo { public static void main(String[] args) throws ClassNotFoundException { //使用类的class属性来获取该类对应的Class对象 Class c1 = Student.class; System.out.println(c1); Class c2 = Student.class; System.out.println(c1 == c2); System.out.println("--------"); //调用对象的getClass()方法,返回该对象所属类对应的Class对象 Student s = new Student(); Class c3 = s.getClass(); System.out.println(c1 == c3); System.out.println("--------"); //使用Class类中的静态方法forName(String className) Class c4 = Class.forName("com.cici.Student"); System.out.println(c1 == c4); } }
反射获取构造方法并使用
- Class类获取构造方法对象
方法名 说明 Constructor<?>[] getConstructors() 返回所有公共构造方法对象的数组 Constructor<?>[] getDeclaredConstructors() 返回所有构造方法对象的数组 Constructor getConstructor(Class<?>... parameterTypes) 返回单个公共构造方法对象 Constructor getDeclaredConstructor(Class<?>... parameterTypes) 返回单个构造方法对象
- 用于创建对象的方法
方法名 说明 T newInstance(Object...initargs) 根据指定的构造方法创建对象
- 代码实现
public class ReflectDemo01 { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //获取Class对象 Class c = Class.forName("com.cici.Student"); //Constructor<?>[] getConstructors() 返回一个包含Constructor对象的数组, Constructor对象反映了由该 Class对象表示的类的所有公共构造函数 // Constructor<?>[] cons = c.getConstructors(); //Constructor<?>[] getDeclaredConstructors() 返回反映由该Class对象表示的类声明的所有构造函数的 Constructor对象的数组 Constructor<?>[] cons = c.getDeclaredConstructors(); for(Constructor con : cons) { System.out.println(con); } System.out.println("--------"); //Constructor<T> getConstructor(Class<?>... parameterTypes) 返回一个 Constructor对象,该对象反映由该 Class对象表示的类的指定公共构造函数 //Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 返回一个 Constructor对象,该对象反映由此 Class对象表示的类或接口的指定构造函数 //参数:你要获取的构造方法的参数的个数和数据类型对应的字节码文件对象 Constructor<?> con = c.getConstructor(); //Constructor提供了一个类的单个构造函数的信息和访问权限 //T newInstance(Object... initargs) 使用由此 Constructor对象表示的构造函数,使用指定的初始化参数来创建和初始化构造函数的声明类的新实例 Object obj = con.newInstance(); System.out.println(obj); // Student s = new Student(); // System.out.println(s); } }
- 反射获取构造方法并使用案例
案例需求
通过反射获取私有构造方法并创建对象
代码实现
学生类
public class Student { //成员变量:一个私有,一个默认,一个公共 private String name; int age; public String address; // 有参和无参构造... // get和set方法... }
测试类
public class ReflectDemo02 { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //获取Class对象 Class<?> c = Class.forName("com.cici.Student"); //public Student(String name, int age, String address) //Constructor<T> getConstructor(Class<?>... parameterTypes) Constructor<?> con = c.getConstructor(String.class,int.class, String.class); //基本数据类型也可以通过.class得到对应的Class类型 //T newInstance(Object... initargs) Object obj = con.newInstance("林青霞", 30, "西安"); System.out.println(obj); } }
反射获取成员变量并使用
- Class类获取成员变量对象的方法
方法名 说明 Field[] getFields() 返回所有公共成员变量对象的数组 Field[] getDeclaredFields() 返回所有成员变量对象的数组 Field getField(String name) 返回单个公共成员变量对象 Field getDeclaredField(String name) 返回单个成员变量对象 void set(Object obj,Object value) 给obj对象的成员变量赋值为value
- 代码演示
public class ReflectDemo01 { public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //获取Class对象 Class<?> c = Class.forName("com.cici.Student"); //Field[] getFields() 返回一个包含 Field对象的数组, Field对象反映由该 Class对象表示的类或接口的所有可访问的公共字段 //Field[] getDeclaredFields() 返回一个 Field对象的数组,反映了由该 Class对象表示的类或接口声明的所有字段 // Field[] fields = c.getFields(); Field[] fields = c.getDeclaredFields(); for(Field field : fields) { System.out.println(field); } System.out.println("--------"); //Field getField(String name) 返回一个 Field对象,该对象反映由该 Class对象表示的类或接口的指定公共成员字段 //Field getDeclaredField(String name) 返回一个 Field对象,该对象反映由该 Class对象表示的类或接口的指定声明字段 Field addressField = c.getField("address"); //获取无参构造方法创建对象 Constructor<?> con = c.getConstructor(); Object obj = con.newInstance(); // obj.addressField = "西安"; //Field提供有关类或接口的单个字段的信息和动态访问 //void set(Object obj, Object value) 将指定的对象参数中由此Field对象表示的字段设置为指定的新值 addressField.set(obj,"郑州"); //给obj的成员变量addressField赋值为西安 System.out.println(obj); } }
反射获取成员方法并使用
- Class类获取成员方法对象的方法
方法名 说明 Method[] getMethods() 返回所有公共成员方法对象的数组,包括继承的 Method[] getDeclaredMethods() 返回所有成员方法对象的数组,不包括继承的 Method getMethod(String name, Class<?>... parameterTypes) 返回单个公共成员方法对象 Method getDeclaredMethod(String name, Class<?>... parameterTypes) 返回单个成员方法对象 Object invoke(Object obj,Object... args) 调用obj对象的成员方法,参数是args,返回值是Object类型
- 代码演示
public class ReflectDemo01 { public static void main(String[] args) throws Exception { //获取Class对象 Class<?> c = Class.forName("com.cici.Student"); //Method[] getMethods() 返回一个包含 方法对象的数组, 方法对象反映由该 Class对象表示的类或接口的所有公共方法,包括由类或接口声明的对象以及从超类和超级接口继承的类 //Method[] getDeclaredMethods() 返回一个包含 方法对象的数组, 方法对象反映由 Class对象表示的类或接口的所有声明方法,包括public,protected,default(package)访问和私有方法,但不包括继承方法 // Method[] methods = c.getMethods(); Method[] methods = c.getDeclaredMethods(); for(Method method : methods) { System.out.println(method); } System.out.println("--------"); //Method getMethod(String name, Class<?>... parameterTypes) 返回一个 方法对象,该对象反映由该 Class对象表示的类或接口的指定公共成员方法 //Method getDeclaredMethod(String name, Class<?>... parameterTypes) 返回一个 方法对象,它反映此表示的类或接口的指定声明的方法Class对象 //public void method1() Method m = c.getMethod("method1"); //获取无参构造方法创建对象 Constructor<?> con = c.getConstructor(); Object obj = con.newInstance(); // obj.m(); //在类或接口上提供有关单一方法的信息和访问权限 //Object invoke(Object obj, Object... args) 在具有指定参数的指定对象上调用此 方法对象表示的基础方法 //Object:返回值类型 //obj:调用方法的对象 //args:方法需要的参数 m.invoke(obj); } }
- 反射获取成员方法案例
需求
通过反射获取私有成员方法并调用
代码实现
public class ReflectDemo02 { public static void main(String[] args) throws Exception { //获取Class对象 Class<?> c = Class.forName("com.cici.Student"); Constructor<?> con = c.getConstructor(); Object obj = con.newInstance(); Method m1 = c.getMethod("method1"); m1.invoke(obj); Method m2 = c.getMethod("method2", String.class); m2.invoke(obj,"林青霞"); Method m3 = c.getMethod("method3", String.class,int.class); Object o = m3.invoke(obj, "林青霞", 30); System.out.println(o); Method m4 = c.getDeclaredMethod("function"); m4.setAccessible(true); m4.invoke(obj); } }
反射综合案例
案例需求
通过反射运行配置文件中指定类的指定方法
代码实现
public class ReflectTest02 { public static void main(String[] args) throws Exception { //加载数据 Properties prop = new Properties(); FileReader fr = new FileReader("myReflect\\class.txt"); prop.load(fr); fr.close(); /* className=com.cici.Student methodName=study */ String className = prop.getProperty("className"); String methodName = prop.getProperty("methodName"); //通过反射来使用 Class<?> c = Class.forName(className);//com.cici.Student Constructor<?> con = c.getConstructor(); Object obj = con.newInstance(); Method m = c.getMethod(methodName);//study m.invoke(obj); } }
范例-反射
package day7; import java.lang.reflect.Method; public class Test11_Reflect { public static void main(String[] args) throws Exception { m1("org.example.Cat","eat"); } //反射经常应用于框架底层的源码,在框架中可能会调用程序员写好的代码,但是框架设计之初以不知道程序员将来写的类和方法叫什么 //可以通过反射来完成,程序员写好类和方法后,按照要求放到配置文件中即可 public static void m1(String clzName, String methoName) throws Exception { Class clz = Class.forName(clzName); //获取类对象 Worker Object o = clz.newInstance(); //创建类对象 Worker s1 = new Woker(); Method method = clz.getMethod(methoName); //获取类中的某个方法 eat() method.invoke(o); //反射调用类中的方法s1.eat() } }
小结
反射
- 获取Class类对象的三种方式
- 类名.class
- 对象名.getClass()
- Class.forName("全类目") 【重要】
- 类名.class
- 反射获取构造方法
- c.getDeclaredConstructors(); 获取类中的所有构造方法
- c.getDeclaredConstructors(); 获取类中的所有构造方法
- Class类获取成员变量对象的方法
- c.getDeclaredFields(); 获取类中所有方法
- c.getFied("address"); 获取类中的指定方法
- c.getDeclaredFields(); 获取类中所有方法
- Class类获取成员方法
- c.getDeclaredMethods(); 获取类中的所有方法
- c.getMethod("methodism"); 获取类中的指定方法
- c.getDeclaredMethods(); 获取类中的所有方法
- Method对象
- method.invoke(); 动态调用方法
- method.invoke(); 动态调用方法
Java 基础
主要内容:
- java 语言概述
- 基础语法
- BasicDataTypes-基本数据类型举例(int, boolean, char)
- Parameter-定义变量、初始化、赋值
- MathOperators-运算符,着重算术运算符和优先级的掌握(++/–,优先级)
- RelationOperators-运算符,着重关系运算符的掌握(==/>/<)
- LogicalOperators-运算符,着重逻辑运算符的掌握(&/|/!,与或非)
- BasicDataTypes-基本数据类型举例(int, boolean, char)
- 流程控制
- IfDemo-if分支(if/else)
- SwitchDemo-switch分支(switch/case)
- ForDemo-for循环(for)
- WhileDemo-while循环(while/do.while)
- IfDemo-if分支(if/else)
- 数组和字符串
- array/ArrayDemo-数组初始化、遍历、工具类
- GetSecondMaxNum-获取数组中的次大数
- BubbleSort-冒泡排序(分析,思路,步骤,代码)
- string/StringDemo-字符串
- array/ArrayDemo-数组初始化、遍历、工具类
- 类和对象
- 集合
- 异常
Java语言概述
Java环境准备
环境准备只需三步:
第一步:jdk 1.8
第二步:idea
第三步:检查环境是否配置正确
JDK 1.8
下载路径:JDK 1.8 https://www.oracle.com/java/technologies/downloads/#java8
JDK历史版本下载: https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html
安装完成后,配置环境变量
- 打开系统环境变量,添加几个变量
- JAVA_HOME=C:\Program Files\Java\jdk1.8
- CLASSPATH=.;%JAVA_HOME%\lib
- JAVA_HOME=C:\Program Files\Java\jdk1.8
- 修改PATH变量的值
- PATH=%JAVA_HOME%\bin
- PATH=%JAVA_HOME%\bin
- 检查:cmd启动控制台后,运行java -version,java version "1.8.."有上述字样,说明java安装和配置成功
Linux中修改 ~/.bashrc 中的环境变量即可
安装Maven
安装IDEA
Hello World– java开始之旅
1.新建一个java项目,直接点击 New Project
- 选择Java,sdk选择之前我们安装好的 1.8, 之后点击 Next
- 直接点击Next
- 给项目进行命名,管理工具选maven,并点击 Create
- 创建好之后我们会直接进入到开发界面
- 右键点击 src 选择 New -> Package,并进行命名 cici.demo
- 在创建好的package下新建 Java Class 文件
- 我们这里新建为 HelloWorld, 注意:文件名需要和Java代码的类名保持一致
- 选择右上角 Add Configuration ,在弹出的窗口中选择 Application
10.需要先在代码中写好 main 函数,之后再进行选择
- 设置项目名称,选择main函数
- 运行代码
Java 基础语法
package cici.demo;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
public 公开的,外部可以调用
static 静态的,代码文件内容加载到内存中
void 表示函数返回值的类型,void 表示空,主函数本身就没有返回值,所以为空。
main 函数名。入口
Java 基础语法
一个 Java 程序可以认为是一系列对象的集合,而这些对象通过调用彼此的方法来协同工作。下面简要介绍下类、对象、方法和实例变量的概念。
- 对象 :对象是类的一个实例,有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。
- 类 :类是一个模板,它描述一类对象的行为和状态。
- 方法 :方法就是行为,一个类可以有很多方法。逻辑运算、数据修改以及所有动作都是在方法中完成的。
- 实例变量 :每个对象都有独特的实例变量,对象的状态由这些实例变量的值决定。
编写 Java 程序时,应注意以下几点:
- 大小写敏感 :Java 是大小写敏感的,这就意味着标识符 Hello 与 hello 是不同的。
- 类名 :对于所有的类来说,类名的首字母应该大写。如果类名由若干单词组成,驼峰式写法,那么每个单词的首字母应该大写,例如 MyFirstJavaClass 。
- 方法名 :所有的方法名都应该以小写字母开头。如果方法名含有若干单词,则后面的每个单词首字母大写。
- 源文件名 :源文件名必须和类名相同。当保存文件的时候,你应该使用类名作为文件名保存(切记 Java 是大小写敏感的),文件名的后缀为 .java。(如果文件名和类名不相同则会导致编译错误)。
- 主方法入口 :所有的 Java 程序由 public static void main(String[] args) 方法开始执行。
例1:BasicDataTypes – 基本数据类型举例(int, boolean, char)
BasicDataTypes.java文件:
package cici.demo; public class BasicDataTypes { public static void main(String[] args) { byte bt = 127; System.out.println(bt); int a = 1; int b = 2; int c = a + b; System.out.println("c = " + c); boolean flag = true; System.out.println("flag = " + flag); flag = false; System.out.println("flag now = " + flag); char c1 = '安'; System.out.println(c1); } }
例2:Parameter – 定义变量、初始化、赋值(初始化)
Parameter.java文件内容 :
package cici.demo; public class Parameter { public static void main(String[] args) { int a = 3; // int b; System.out.println("a = " + a); // System.out.println("b = " + b); } }
例3:MathOperators– 运算符
package cici.demo; public class MathOperators { public static void main(String[] args) { int a = 1; System.out.println("a = " + a); int b = a++; //a++ ==> a = a + 1 System.out.println("a = " + a); System.out.println("b = " + b); System.out.println("int b = a++ 可分解为 int b = a; \n\t\t\t\t\t a = a+1;"); System.out.println("************************************************"); int c = 10; System.out.println("c = " + c); int d = ++c; System.out.println("c = " + c); System.out.println("d = " + d); System.out.println("int d = ++c 可分解为 c = c + 1; \n\t\t\t\t int d = c;"); //d-- ==> d = d - 1 d--; System.out.println("d = " + d); } }
例4:RelationOperators – 运算符
package cici.demo; public class RelationOperators { public static void main(String[] args) { int a = 90; int b = 90; if (a == b) { System.out.println("a equals b"); } b--; if (a > b) { System.out.println("a > b"); } if (a < b) { System.out.println("a < b"); } } }
例5:LogicalOperators– 运算符
package com.test.basic.chapter2; public class LogicalOperators { public static void main(String[] args) { int a = 100; int b = 100; System.out.println(a > b && a > 99); //a大于b并且a大于99 if (a > b && a > 99) { System.out.println("a > 99 and a > b"); } //a大于b或者是a大于99 System.out.println(a > b || a > 99); if (a > b || a > 99) { System.out.println("a > 99 but not a > b"); } //a小于等于b System.out.println((a > b)); System.out.println(!(a > b)); // !(a > b) ==> (a <= b) if (!(a > b)) { System.out.println("not a > b"); } } }
数组和字符串
[array/ArrayDemo] – 数组 初始化、遍历、工具类
package cici.arrays; import java.util.Arrays; /** * 功能:介绍数组 * 注意:数组是在内存中是一⽚连续的空间,下标从0开始 */ public class ArrayDemo { public static void main(String[] args) { int[] arr = new int[]{1,2,9,5,3}; System.out.println(arr.toString()); int sum = 0; for (int i = 0; i < arr.length; i++) { sum += arr[i]; } System.out.println("数组的和为:" + sum); Arrays.sort(arr); System.out.println("用工具类Arrays进行排序后的结果:"); for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + ","); } } }
[GetSecondMaxNum]– 获取数组中的次大数
package cici.arrays; /** * 功能:对于给定的一个数组,找到其中的次大数,并输出 * 思路: * 想类似的情况:找到最大数,这个比较好做,一趟比较下来,能获得最大数 * 那次大数如何获得,应该是在最大数的情况下,多一次比较,比最大数小的那个就是次大数了 * 1. 通过for循环取出当前数,把当前数跟最大数和次大数进行比较 * 2. 另声明一个变量,保存比最大数小的那个数 * 3. 比最大数大,则次大数为之前的最大数,最大数为当前数;比最大数小,比次大数大,则次大数为当前数 */ public class GetSecondMaxNum { public static void main(String[] args) { int[] array = {4, 3, 8, 1, 10, 6}; for (int i = 0; i < array.length; i++) { System.out.print(array[i] + " "); } int max = array[0]; int secondMax = 0; for (int i = 1; i < array.length; i++) { //当前元素是最大数,需要更新最大数 if (array[i] > max) { secondMax = max; max = array[i]; } else if (array[i] <= max && array[i] >= secondMax) { //当前元素比次大数大,更新次大数 secondMax = array[i]; } } System.out.println("max=" + max); System.out.println("second max=" + secondMax); } }
[BubbleSort]– 冒泡排序(分析,思路,步骤,代码)
package cici.arrays; /** * 功能:冒泡排序 从小到大排 * 思路:相邻两个数比较,左边比右边大则交换,整体比较完毕是一次排序 * 这样的排序要进行n-1趟 */ public class BubbleSort { public static void main(String[] args) { int[] array = new int[]{63, 4, 24, 1, 3, 13}; System.out.println("冒泡排序法从小到大排序的过程是:"); //i是趟数 for (int i = 1; i < array.length; i++) { //j是一趟中比较次数 for (int j = 0; j < array.length - i; j++) { if (array[j] > array[j + 1]) { //swap int temp = array[j]; array[j] = array[j + 1]; array[j + 1] = temp; } System.out.print(array[j] + " "); } System.out.print("【"); for (int j = array.length - i; j < array.length; j++) { System.out.print(array[j] + " "); } System.out.println("】"); } } }
[string/StrinDemo]– 字符串
package cici.arrays; /** * 功能:介绍字符串 */ public class StringDemo { public static void main(String[] args) { String str = new String("abc"); //初始化 String str1 = "abc"; //初始化 System.out.println("str == str1? 是否同一个对象:" + (str == str1)); System.out.println("纯字符串比较:" + ("abc" == "abc")); System.out.println("对象str1和字符串比较:" + (str1 == "abc")); System.out.println("对象str和字符串比较:" + (str == "abc")); System.out.println("字符串长度:" + str.length()); System.out.println("字符c在字符串str中的位置(下标):" + str.indexOf("c")); System.out.println("字符串str第2个位置是什么字符:" + str.charAt(1)); str = " |" + str + "| "; System.out.println("加上空格后的str:" + str); System.out.println("去除str两头的空格后:" + str.trim()); System.out.println("把|都去除:" + str.replace("|", "")); System.out.println("str的内容与str1的内容是否相同:" + str1.equals(str.trim().replace("|", ""))); String s = "abcd,efgh,dddj"; String[] sArray = s.split(","); System.out.println(s + " 分割后结果:"); for (String item : sArray) { System.out.println(item); } } }
流程控制
例1:IfDemo– if分支(if/else)
package cici.flow.control; public class IfDemo { public static void main(String[] args) { int age = Integer.parseInt(args[0]); System.out.println("您输入的年龄是:" + age); System.out.print("您是:"); if (age < 8) { System.out.println("学龄前儿童"); } else if (age >= 8 && age < 14) { System.out.println("小学生"); } else if (age >= 14 && age < 20) { System.out.println("中学生"); } else if (age >= 20 && age < 25) { System.out.println("大学生"); } else { System.out.println("职场人"); } } }
例2:SwitchDemo– switch分支(switch/case)
package cici.flow.control; public class SwitchDemo { public static void main(String[] args) { int level = Integer.parseInt(args[0]); System.out.println("您输入的年龄阶段是:" + level); System.out.print("您是:"); switch (level){ case 1: System.out.println("学龄前儿童"); break; case 2: System.out.println("小学生"); break; case 3: System.out.println("中学生"); break; case 4: System.out.println("大学生"); break; default: System.out.println("职场人"); } } }
例3:ForDemo – for循环(for)
package cici.flow.control; public class ForDemo { public static void main(String[] args) { for(int x = 10; x < 20; x = x+1) { System.out.print("value of x : " + x ); System.out.print("\n"); } } }
例4:WhileDemo – while循环(while/do.while)
package cici.flow.control; public class WhileDemo { public static void main(String[] args) { int sum = 0; int i = 1; while (i <= 100) { sum += i++; } System.out.println("1+2+3+...+100=" + sum); System.out.println(i); // do while int doSum = 0; int j = 1; do { doSum += j++; } while (j <= 100); System.out.println("1+2+3+...+100=" + doSum); } }
例5:SkipDemo– break continue return 跳转语句
package cici.flow.control; public class SkipDemo { public static void main(String[] args) { int a = 1; while (true) { a++; //break //当a大于3的时候,跳出循环 if (a > 3) { break; } } System.out.println(a); a = 1; while (a < 10) { a++; //当a的值能被2整除,表示该数不是奇数 if (a % 2 == 0) { continue; //跳过下面的语句,进入下一次循环 } System.out.print(a + " "); } return; // System.out.println("sss"); } }
类和对象
例1:ClassDemo – 类和对象的概念
package cici.classd; public class Person { String name; //姓名 int age; //年龄 String gender; //性别 “男”, public void speak() { System.out.println( "我是" + this.name + ",今年" + this.age + "岁"); } }
package cici.classd; public class ClassDemo { public static void main(String[] args) { Person xiaoming = new Person(); System.out.println(xiaoming); Person xiaowang = new Person(); System.out.println(xiaowang); } }
例2:PropertyDemo – 成员变量
package cici.classd; public class PropertyDemo { public static void main(String[] args) { Person xiaoming = new Person(); xiaoming.name = "小明"; xiaoming.age = 21; xiaoming.gender = "男"; System.out.println("姓名:" + xiaoming.name + " 年龄:" + xiaoming.age + " 性别:" + xiaoming.gender); Person b; b = xiaoming; System.out.println(b.name); b.age = 200; System.out.println(xiaoming.age); System.out.println(b); System.out.println(xiaoming); } }
例3:MethodDemo – 成员方法
package cici.classd; public class MethodDemo { public static void main(String[] args) { Person xiaoming = new Person(); xiaoming.name = "小明"; xiaoming.age = 21; xiaoming.gender = "男"; System.out.print(xiaoming.name + "说:"); xiaoming.speak(); } }
例4:Constructor – 构造函数
package cici.classd; /** * * 功能:构造函数 * 总结: * 1.构造函数名和类名相同 * 2.构造函数没有返回值 * 3.对新对象的初始化 * 4.在创建新对象时,系统自动的调用该类的构造函数 * 5.一个类可以有多个构造函数 * 6.每个类都有一个默认的构造函数 */ public class PersonC { String name; int age; PersonC(String pname, int page) { name = pname; age = page; } String speak() { return "我是" + name + ",今年" + age + "岁"; } }
package cici.classd; public class ConstructorDemo { public static void main(String[] args) { PersonC p = new PersonC("小明", 25); System.out.println(p.speak()); } }
例5:ThisDemo – this在类中的使用
package cici.classd; /** * 功能:this代表当前对象 * 注意:this不能在类定义的外部使用,只能在类定义的方法中使用 */ class PersonT { String name; int age; PersonT(String name, int age) { this.name = name; this.age = age; } PersonT(String name) { this(name, 1); } String speak() { return "我是" + this.name + ",今年" + this.age + "岁"; } }
package cici.classd; public class ThisDemo { public static void main(String[] args) { PersonT p1 = new PersonT("小王", 30); System.out.println(p1.speak()); } }
例6:StaticDemo – static静态变量、静态方法
定义: 在类中使用static修饰的静态方法会随着类的定义而被分配和装载入内存中;而非静态方法属于对象的具体实例,只有在类的对象创建时在对象的内存中才有这个方法的代码段。
package cici.classd; class Student { static int totalNo = 0; String name; int age; Student(String name, int age) { totalNo++; } public static void speak() { System.out.println("我是红领巾。"); } public int getTotalNo() { return totalNo; } }
package cici.classd; public class StaticDemo { public static void main(String[] args) { Student s1 = new Student("小明", 10); Student s2 = new Student("小王", 13); System.out.println("总人数:" + s2.getTotalNo()); Student.speak(); } }
静态方法,系统会为静态方法分配一个固定的内存空间。而普通方法,会随着对象的调用而加载,当使用完毕,会自动释放掉空间。普通方法的好处是,动态规划了内存空间的使用,节省内存资源。静态方法,方便,运行快,而如果全部方法都用静态方法,那么每个方法都要有一个固定的空间,这样的话太占内存。
因而也就解释了,为什么静态方法可以直接被类名调用,而不需要用对象调用, 因为他有固定空间,随类的加载而加载。
静态方法不需要对象,它在你定义对象就有了,因此就可以方便地直接类名调用。不需要实例化对象。
例8:extendsDemo/PersonsMain – 小学生继承自人,构造函数的继承及引用方式
package cici.classd; public class Human { private String name; private char gender; private int age; // public Human() { // System.out.println("Human none con"); // } public Human(String name, char gender, int age) { System.out.println("Human..."); this.name = name; this.gender = gender; this.age = age; } protected String getName() { return name; } public void think() { System.out.println(this.name + "在思考。。。"); } }
package cici.classd; public class Pupil extends Human{ private String studentNo; // public Pupil() { // System.out.println("Pupil none con"); // } public Pupil(String studentNo, String name, char gender, int age) { super(name, gender, age); //super用来调用父类构造方法,必须是第一句 this.studentNo = studentNo; System.out.println("Pupil...."); } public void learn() { System.out.println(this.getName() + "在学习。。。"); } }
package cici.classd; public class PersonsMain { public static void main(String[] args) { Human person = new Human("林冲", '男', 30); person.think(); Pupil liming = new Pupil("001", "李明", '男', 8); liming.think(); liming.learn(); } }
例9:overloadVSoverride/OverloadDemo – 重载
Java 允许同一个类中定义多个同名方法,只要它们的形参列表不同即可。如果同一个类中包含了两个或两个以上方法名相同的方法,但形参列表不同,这种情况被称为方法重载(overload)。
package cici.classd; public class OverloadDemo { public static void main(String[] args) { int a = 5; int b = 10; getMax(a, b); float c = 5f; float d = 10f; getMax(c, d); } private static void getMax(int firstNum, int lastNum) { if (firstNum > lastNum) { System.out.println("较大的数是" + firstNum); } else if (firstNum < lastNum) { System.out.println("较大的数是" + lastNum); } else { System.out.println("两个数相等"); } } //只有方法的返回类型不同不能算是重载 // private static int getMax(int firstNum, int lastNum) {return 1;} private static void getMax(float firstNum, float lastNum) { if (firstNum > lastNum) { System.out.println("较大的数是" + firstNum); } else if (firstNum < lastNum) { System.out.println("较大的数是" + lastNum); } else { System.out.println("两个数相等"); } } //只有方法的修饰符不同不能算是重载 // public static void getMax(float firstNum, float lastNum) {} }
例10:overloadVSoverride/OverrideDemo – 重写
在子类中如果创建了一个与父类中相同名称、相同返回值类型、相同参数列表的方法,只是方法体中的实现不同,以实现不同于父类的功能,这种方式被称为方法重写(override),又称为方法覆盖。当父类中的方法无法满足子类需求或子类具有特有功能的时候,需要方法重写。
package cici.classd; public class Animal { private String name; private int age; //动物都会叫唤 public void cry() { System.out.println("动物都会叫唤,但是具体的某一个种类的动物叫唤方式不同,需要重写我的cry方 法"); } }
package cici.classd; public class Cat extends Animal{ @Override public void cry() { System.out.println("猫猫叫!"); } }
package cici.classd; public class OverrideDemo { public static void main(String[] args) { Cat cat = new Cat(); cat.cry(); } }
例11:multiStatus/MultiStatusDemo – 多态,处理问题的方式
多态性是面向对象编程的又一个重要特征,它是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。
package cici.classd; public class Dog extends Animal{ @Override public void eat() { System.out.println("狗吃:"); } }
package cici.classd; public class Bone extends Food { @Override public void showName() { System.out.println("食物是骨头"); } }
package cici.classd; public class Fish extends Food { @Override public void showName() { System.out.println("食物是鱼"); } }
package cici.classd; public class Master { //给某种动物喂相应的食物 public void feed(Animal animal, Food food) { System.out.println("主人喂"); animal.eat(); food.showName(); } }
package cici.classd; public class CatM extends Animal { @Override public void eat() { System.out.println("猫吃:"); } }
package cici.classd; public class MultiStatusDemo { public static void main(String[] args) { Master master = new Master(); Dog dog = new Dog(); Bone bone = new Bone(); master.feed(dog, bone); System.out.println("**********************"); master.feed(new Cat(), new Fish()); } }
- 多态就是同一个接口,使用不同的实例而执行不同操作
多态存在的三个必要条件
1.继承
2.重写
3.父类引用指向子类对象
安全分享
多态时,如何审计?
人工代码审计的时候,有多态的情况,有什么情况?
人工审计时会看对应的继承和重写
自动化审计方式,代码扫描时,能否解决多态问题?
例12:abstractDemo/AbstractDemo – 抽象类,关键字 abstract
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,那么这样的类称为抽象类。
在 Java 中抽象类的语法格式如下:
<abstract>class<class_name> { <abstract><type><method_name>(parameter-iist); }
其中,abstract 表示该类或该方法是抽象的;class_name 表示抽象类的名称;method_name 表示抽象方法名称,parameter-list 表示方法参数列表。
package cici.classd; public class CatA extends AnimalA { @Override public void cry() { System.out.println("猫猫叫"); } }
package cici.classd; public abstract class AnimalA { String name; int age; //动物会叫 public abstract void cry(); }
package cici.classd; public class AbstractDemo { public static void main(String[] args) { //抽象类不能被实例化 // AnimalA a = new AnimalA(); Animal cat = new Cat(); cat.cry(); } }
抽象类有什么作用?(结合多态思考)
抽象类是用来捕捉子类的通用特性的,是被用来创建继承层级里子类的模板。现实中有些父类中的方法确实没有必要写,因为各个子类中的这个方法肯定会有不同;而写成抽象类,这样看代码时,就知道这是抽象方法,而知道这个方法是在子类中实现的,所以有提示作用。
例13:interfaceDemo/InterfaceDemo – 接口
抽象类是从多个类中抽象出来的模板,如果将这种抽象进行的更彻底,则可以提炼出一种更加特殊的“抽象类”——接口(Interface)。接口是 Java 中最重要的概念之一,它可以被理解为一种特殊的类,不同的是接口的成员没有执行体,是由全局常量和公共的抽象方法所组成。
定义接口
Java 接口的定义方式与类基本相同,不过接口定义使用的关键字是 interface,接口定义的语法格式如下:
[public] interface interface_name [extends interface1_name[, interface2_name,…]] { // 接口体,其中可以包含定义常量和声明方法 [public] [static] [final] type constant_name = value; // 定义常量 [public] [abstract] returnType method_name(parameter_list); // 声明方法 }
对以上语法的说明如下:
- public 表示接口的修饰符,当没有修饰符时,则使用默认的修饰符,此时该接口的访问权限仅局限于所属的包;
- interface_name 表示接口的名称。接口名应与类名采用相同的命名规则,即如果仅从语法角度来看,接口名只要是合法的标识符即可。如果要遵守 Java 可读性规范,则接口名应由多个有意义的单词连缀而成,每个单词首字母大写,单词与单词之间无需任何分隔符。
- extends 表示接口的继承关系;
- interface1_name 表示要继承的接口名称;
- constant_name 表示变量名称,一般是 static 和 final 型的;
- returnType 表示方法的返回值类型;
- parameter_list 表示参数列表,在接口中的方法是没有方法体的。
注意:一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
接口的主要用途就是被实现类实现,一个类可以实现一个或多个接口,继承使用 extends 关键字,实现则使用implements 关键字。因为一个类可以实现多个接口,这也是 Java 为单继承灵活性不足所作的补充。
package cici.classd.interfaceDemo; public abstract class AbstractUSB { abstract void start(); }
package cici.classd.interfaceDemo; public class Camera implements IUSB { public void start() { System.out.println("我是照相机,开始工作了。"); } public void stop() { System.out.println("我是照相机,停止工作了。"); } }
package cici.classd.interfaceDemo; public class Computer { public Computer() { System.out.println("计算机启动了"); } public void useUSB(IUSB usb) { usb.start(); usb.stop(); } public void useAb(AbstractUSB ausb) { ausb.start(); } }
package cici.classd.interfaceDemo; public class Fen extends AbstractUSB { @Override void start() { System.out.println("feng shan"); } }
package cici.classd.interfaceDemo; public interface IUSB { //开始工作 public void start(); //停止工作 public void stop(); }
package cici.classd.interfaceDemo; public class Phone implements IUSB{ public void start() { System.out.println("我是照相机,开始工作了。"); } public void stop() { System.out.println("我是照相机,停止工作了。"); } }
package cici.classd.interfaceDemo; public class InterfaceDemo { public static void main(String[] args) { Computer computer = new Computer(); IUSB phone = new Phone(); computer.useUSB(phone); //计算机连接照相机 IUSB camera = new Camera(); computer.useUSB(camera); //计算机连接风扇 AbstractUSB fen = new Fen(); computer.useAb(fen); } }
Java 中的接口有什么作用?
例如我定义了一个接口,但是我在继承这个接口的类中还要写接口的实现方法,那我不如直接就在这个类中写实现方法岂不是更便捷,还省去了定义接口?
接口就是个招牌。
接口只是一个规范,所以里面的方法都是空的。
比如说你今年放假出去旅游,你有点饿了,突然看到前面有个店子,上面挂着必胜客,然后你就知道今天中饭有着落了。
必胜客就是接口,我们看到了这个接口,就知道这个店会卖炸鸡、汉堡(实现接口)。
那么为什么我们要去定义一个接口呢,这个店可以直接卖炸鸡、汉堡(直接写实现方法)。
是的,这个店可以直接卖炸鸡,但没有挂必胜客的招牌,我们就不能直接简单粗暴的冲进去叫服务员给两个炸鸡。
要么,我们就要进去问,你这里卖不卖炸鸡腿啊,卖不卖汉堡啊,卖不卖圣代啊(这就是反射)。很显然,这样一家家的问实在是非常麻烦(反射性能很差)。
要么,我们就要记住,灵境胡同108号卖炸鸡,牛街45号卖炸鸡(硬编码),很显然这样我们要记住的很多很多东西(代码量剧增),而且,如果有新的店卖炸鸡腿,我们也不可能知道(不利于扩展)。
什么时候使用接口
如果你想实现多重继承,那么你必须使用接口。由于Java不支持多继承,子类不能够继承多个类,但可以实现多个接口。因此你就可以使用接口来解决它。
super关键字的使用
- 调用子类中重写的父类的方法。
- 如果超类(superclass)和子类(subclass)都有同名的属性,则访问超类的属性(字段)。
- 从子类构造函数显式地调用超类无参数化构造函数或参数化构造函数。
下面让我们了解所有这些用途。
1.访问超类的重写方法
如果在超类和子类中都定义了相同名称的方法,则子类中的方法将覆盖超类中的方法。这称为方法重写。
示例1:方法重写
class Animal { //方法 public void display(){ System.out.println("I am an animal"); } } class Dog extends Animal { //重写方法 @Override public void display(){ System.out.println("I am a dog"); } public void printMessage(){ display(); } } class Main { public static void main(String[] args) { Dog dog1 = new Dog(); dog1.printMessage(); } }
输出结果
I am a dog
在本示例中,通过创建Dog类的对象dog1,我们可以调用它的方法printMessage(),然后该方法执行display()语句。
由于display()在两个类中都定义,所以子类Dog的方法覆盖了超类Animal的方法。因此,调用了子类的display()。
如果需要调用超类Animal的重载方法display(),则使用super.display()。
示例2:super调用超类方法
class Animal { //方法 public void display(){ System.out.println("I am an animal"); } } class Dog extends Animal { //重写方法 @Override public void display(){ System.out.println("I am a dog"); } public void printMessage(){ //这调用重写方法 display(); // 这调用父类的方法 super.display(); } } class Main { public static void main(String[] args) { Dog dog1 = new Dog(); dog1.printMessage(); } }
输出结果
I am a dog I am an animal
在这里,上述程序是如何工作的。
2.访问超(父)类的属性
超类和子类可以具有相同名称的属性。我们使用super关键字来访问超类的属性。
示例3:访问超类属性
class Animal { protected String type="动物"; } class Dog extends Animal { public String type="哺乳动物"; public void printType() { System.out.println("我是 " + type); System.out.println("我是一只 " + super.type); } } class Main { public static void main(String[] args) { Dog dog = new Dog(); dog.printType(); } }
输出:
我是哺乳动物 我是一只动物
在这个实例中,我们在超类Animal和子类Dog中定义了相同的实例字段类型。
然后我们创建了Dog类的对象dog。然后,使用此对象调用printType()方法。
在printType()函数内部,
- type - 指的是子类Dog的属性。
- super.type - 指超类Animal的属性。
因此,System.out.println("我是 " + type);输出 “我是哺乳动物”,并且,System.out.println("我是一只 " + super.type);打印输出“我是一只动物”。
3.使用super()访问超类构造函数
众所周知,创建类的对象时,将自动调用其默认构造函数。
要从子类构造函数中显式调用超类构造函数,我们使用super()。这是super关键字的一种特殊形式。
注意:super() 只能在子类构造函数中使用,并且必须是第一条语句。
示例4:使用super()
class Animal { //Animal类的默认或无参数构造函数 Animal() { System.out.println("I am an animal"); } } class Dog extends Animal { // Dog类的默认或无参数构造函数 Dog() { //调用超类的默认构造函数 super(); System.out.println("I am a dog"); } } class Main { public static void main(String[] args) { Dog dog = new Dog(); } }
输出结果
I am an animal I am a dog
在这里,当Dog类的对象dog被创建时,它会自动调用该类的默认或无参数构造函数。
在子类构造函数中,super()语句调用超类的构造函数并执行其中的语句。因此,我们得到的结果“I am ananimal”。
示例5:使用super()调用参数化构造函数
class Animal { //默认或无参数的构造函数 Animal() { System.out.println("I am an animal"); } //参数化构造函数 Animal(String type) { System.out.println("Type: "+type); } } class Dog extends Animal { //默认构造函数 Dog() { //调用超类的参数化构造函数 super("Animal"); System.out.println("I am a dog"); } } class Main { public static void main(String[] args) { Dog dog1 = new Dog(); } }
输出结果
Type: Animal I am a dog
编译器可以自动调用无参数构造函数。但是,它不能调用带有参数的构造函数。
如果必须调用参数化的构造函数,则需要在子类构造函数中显式定义它,如上面代码中的语句:
super("Animal");
请注意,在上面的示例中,我们使用了super("Animal"),显式地调用参数化构造函数。在这种情况下,编译器不会调用超类的默认构造函数。
Java 反射(Reflection)
Java中,反射允许我们在运行时检查和操作类、接口、构造函数、方法和字段。
Java 类名为Class
在学习Java反射之前,我们需要了解一个名为Class的Java类。
Java中有一个名为Class的类,该类在运行时保留有关对象和类的所有信息。
Class对象描述了特定类的属性。该对象用于执行反射。
创建名为Class的类的对象
我们可以创建Class的对象,通过:
使用getClass()方法
getClass()方法使用特定类的对象来创建新的对象Class。例如,
public class test { public static void main(String[] args) { Dog d = new Dog(); Class c = d.getClass(); System.out.println(c.getName()); } }
- 使用.class
我们还可以使用.class扩展名创建Class对象。例如,
Class c = Dog.class; System.out.println(c.getName());
创建Class对象后,我们可以使用这些对象执行反射。
获取接口
我们可以使用Class的getInterfaces()方法来收集类实现的接口的信息。此方法返回一个接口数组。
示例:获取接口
import java.lang.Class; import java.lang.reflect.*; interface Animal { public void display(); } interface Animal2 { public void makeSound(); } class Dog implements Animal, Animal2 { public void display() { System.out.println("I am a dog."); } public void makeSound() { System.out.println("Bark bark"); } } class ReflectionDemo { public static void main(String[] args) { try { //创建一个Dog类的对象 Dog d1 = new Dog(); //使用getClass()创建Class对象 Class obj = d1.getClass(); //查找由Dog实现的接口 Class[] objInterface = obj.getInterfaces(); for(Class c : objInterface) { //打印接口名称 System.out.println("Interface Name: " + c.getName()); } } catch(Exception e) { e.printStackTrace(); } } }
输出结果
Interface Name: Animal Interface Name: Mammal
获取超类和访问修饰符
类Class的方法getSuperclass()可用于获取有关特定类的超类的信息。
而且,Class提供了一种getModifier()方法,该方法以整数形式返回class的修饰符。
示例:获取超类和访问修饰符
package cici.reflect; public class Animal { public void display() {}; } package cici.reflect; public class Dog extends Animal{ @Override public void display() { System.out.println("I am a dog."); } }
package cici.reflect; import java.lang.reflect.Modifier; public class reflectDemo { public static void main(String[] args) { try { //创建一个Dog类的对象 Dog d1 = new Dog(); //使用getClass()创建Class对象 Class obj = d1.getClass(); //以整数形式获取Dog的访问修饰符 int modifier = obj.getModifiers(); System.out.println("修饰符: " + Modifier.toString(modifier)); //找到Dog的超类 Class superClass = obj.getSuperclass(); System.out.println("Superclass: " + superClass.getName()); } catch(Exception e) { e.printStackTrace(); } } }
输出结果
修饰符: public Superclass: Animal
反射字段,方法和构造函数
该软件包java.lang.reflect提供了可用于操作类成员的类。例如
- 方法类 - 提供有关类中方法的信息
- 字段类 - 提供有关类中字段的信息
- 构造函数类 - 提供有关类中构造函数的信息
Java 反射与字段
我们可以使用Field类提供的各种方法检查和修改类的不同字段。
- getFields() - 返回该类及其超类的所有公共字段
- getDeclaredFields() - 返回类的所有字段
- getModifier() - 以整数形式返回字段的修饰符
- set(classObject,value) - 使用指定的值设置字段的值
- get(classObject) - 获取字段的值
- setAccessible(boolean) - 使私有字段可访问
注意:如果我们知道字段名称,则可以使用
- getField("fieldName") - 从类返回名称为fieldName的公共字段。
getDeclaredField*("fieldName") *
- 从类返回名称为fieldName的字段。
示例:访问访问公共字段
package cici.reflect; class Dog { public String type; } package cici.reflect; import java.lang.reflect.Field; import java.lang.reflect.Modifier; public class reflectDemo { public static void main(String[] args) { try { Dog dog = new Dog(); Class obj = dog.getClass(); Field field = obj.getField("type"); field.set(dog, "labrador"); String typeValue = (String)field.get(dog); System.out.println("type: " + typeValue); } catch (Exception e) { e.printStackTrace(); } } }
输出结果
type: labrador
修饰符: public
示例:访问私有字段
package cici.reflect; class Dog { private String color; }
package cici.reflect; import java.lang.reflect.Field; import java.lang.reflect.Modifier; class reflectDemo { public static void main(String[] args) { try { Dog dog = new Dog(); // 创建类Class对象 Class obj = dog.getClass(); // 访问私有字段 Field field2 = obj.getDeclaredField("color"); // 使私有字段可访问 field2.setAccessible(true); // 设置color的值 field2.set(dog, "brown"); // 获取color的值 String colorValue = (String)field2.get(dog); System.out.println("color: " + colorValue); // 获取color的访问修饰符 int mod2 = field2.getModifiers(); String modifier2 = Modifier.toString(mod2); System.out.println("modifier: " + modifier2); } catch(Exception e) { e.printStackTrace(); } } }
输出结果
color: brown modifier: private
安全分享
什么安全场景可以用到这个技术
waf场景: 分析流量过滤并阻断攻击
- 在变量里运用插桩技术,利用反射做内容安全检测,就比较准确,也没有waf的流量压力
- 安全代码插入到研发代码中
- 安全代码插入到研发代码中
Java 反射与方法
像字段一样,我们可以使用Method类提供的各种方法来检查类的不同方法。
- getMethods() - 返回该类及其超类的所有公共方法
- getDeclaredMethod() - 返回该类的所有方法
- getName() - 返回方法的名称
- getModifiers() - 以整数形式返回方法的访问修饰符
- getReturnType() - 返回方法的返回类型
示例:方法反射
package cici.reflect; class Dog { public void display() { System.out.println("I am a dog."); } protected void eat() { System.out.println("I eat dog food."); } private void makeSound() { System.out.println("Bark Bark"); } }
package cici.reflect; import java.lang.Class; import java.lang.reflect.*; class reflectDemo { public static void main(String[] args) { try { Dog dog = new Dog(); //创建一个Class对象 Class obj = dog.getClass(); //使用getDeclaredMethod()获取所有方法 Method[] methods = obj.getDeclaredMethods(); //获取方法的名称 for(Method m : methods) { System.out.println("方法名称: " + m.getName()); //获取方法的访问修饰符 int modifier = m.getModifiers(); System.out.println("修饰符: " + Modifier.toString(modifier)); //获取方法的返回类型 System.out.println("Return Types: " + m.getReturnType()); System.out.println(" "); } } catch(Exception e) { e.printStackTrace(); } } }
输出结果
方法名称: display 修饰符: public Return type: void 方法名称: eat 修饰符: protected 返回类型: void 方法名称: makeSound 修饰符: private 返回类型: void
Java 反射与构造函数
我们还可以使用Constructor类提供的各种方法检查类的不同构造函数。
- getConstructors() - 返回该类的所有公共构造函数以及该类的超类
- getDeclaredConstructor() -返回所有构造函数
- getName() - 返回构造函数的名称
- getModifiers() - 以整数形式返回构造函数的访问修饰符
- getParameterCount()******* - 返回构造函数的参数数量
示例:构造函数反射
package cici.reflect; class Dog { public Dog() { } public Dog(int age) { } private Dog(String sound, String type) { } }
package cici.reflect; import java.lang.Class; import java.lang.reflect.*; class reflectDemo { public static void main(String[] args) { try { Dog d1 = new Dog(); Class obj = d1.getClass(); //使用getDeclaredConstructor()获取一个类中的所有构造函数 Constructor[] constructors = obj.getDeclaredConstructors(); for(Constructor c : constructors) { //获取构造函数的名称 System.out.println("构造函数名称: " + c.getName()); //获取构造函数的访问修饰符 int modifier = c.getModifiers(); System.out.println("修饰符: " + Modifier.toString(modifier)); //获取构造函数中的参数数量 System.out.println("参数个数: " + c.getParameterCount()); } } catch(Exception e) { e.printStackTrace(); } } }
输出结果
构造函数名称: Dog 修饰符: public 参数个数: 0 构造函数名称: Dog 修饰符: public 参数个数: 1 构造函数名称: Dog 修饰符: private 参数个数: 2
集合
集合 vs 数组:数组的长度是固定的,集合的长度是可变的
常用的集合有List集合、Set集合、Map集合,其中List与Set实现了Collection接口
继承关系
常用集合类的继承关系 -------java.lang.Object------- / \ / \ Collection \ / \ \ / \ \ Set List Map / \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \
TreeMapSetDemo
–介绍HashSet的使用方式
package cici.conllection; public class Person { Integer age; Person(int Iage){ age = Iage; } }
package cici.conllection; import java.util.HashSet; import java.util.Set; public class SetDemo { public static void main(String[] args) { Set<String> set = new HashSet<String>(); set.add("test"); set.add("test"); set.add("set1"); set.add("aaa"); set.add("bbb"); //不保证迭代顺序,比方字母排序,数字排序 for (String item : set) { System.out.println(item); } // Set<Person> pSet = new HashSet<Person>(); // Person p1 = new Person(1111111); // Person p2 = new Person(1111111); // pSet.add(p1); // pSet.add(p2); // // for (Person item : pSet) { // System.out.println(item); // } } }
ListDemo
–介绍ArrayList的使用方式
package cici.conllection; import java.util.ArrayList; import java.util.List; public class ListDemo { public static void main(String[] args) { String a = "a"; String b = "b"; String c = "c"; String d = "d"; String apple = "apple"; List<String> list = new ArrayList<String>(); list.add(a); list.add(apple); list.add(b); list.add(apple); list.add(c); list.add(apple); list.add(d); System.out.println(list);//输出列表的全部元素 System.out.println("apple 第一次出现的索引位置是:" + list.indexOf(apple)); System.out.println("apple 最后一次出现的索引位置是:" + list.lastIndexOf(apple)); } }
MapDemo
– 介绍HashMap的使用方式
package cici.conllection; import java.util.HashMap; import java.util.Map; import java.util.Set; public class MapDemo2 { public static void main(String[] args) { Map<String, String> dictionary = new HashMap<String, String>(); dictionary.put("java", "java编程思想"); dictionary.put("c", "c语言"); dictionary.put("shell", "Shell编程"); System.out.println(dictionary.get("java")); for (String value : dictionary.values()) { System.out.println("书籍为:" + value); } Set<String> keys = dictionary.keySet(); for (String key : keys) { System.out.println("书籍代号为:" + key + ", 书籍名称为:" + dictionary.get(key)); } } }
异常
在程序设计和运行的过程中,发生错误是不可避免的。尽管 Java 语言的设计从根本上提供了便于写出整洁、安全代码的方法,并且程序员也尽量地减少错误的产生,但是使程序被迫停止的错误的存在仍然不可避免。
为此,Java 提供了异常处理机制来帮助程序员检查可能出现的错误,以保证程序的可读性和可维护性。
例1:WhatsException– 什么是异常
package cici.unusual; public class WhatsException { public static void main(String[] args) { nullPointerException(); arrayIndexOutOfBoundsException(); } private static void arrayIndexOutOfBoundsException() { int[] a = new int[]{1,2,3}; System.out.println(a[4]); } private static void nullPointerException() { String a = null; a.length(); } }
例2:HandleException – 简单处理异常(try/catch)
package cici.unusual; public class HandleException { public static void main(String[] args) { int x = 100; int y = 0; int z = 0; try { z = x / y; System.out.println(x + "除以" + y + "的商是:" + z); } catch (Exception e) { e.printStackTrace();//输出异常到标准错误流 //使用getMessage()方法输出异常信息 System.out.println("getMessage方法:" + e.getMessage()); } } }
泛型
概述
泛型,就是“参数化类型”。就是将类型由原来的具体的类型参数化(也就是在写类型的地方当成一个参数来写),类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型方法
你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
下面是定义泛型方法的规则:
- 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的)。
- 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
- 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
- 泛型方法方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char 的等)。
实例
下面的例子演示了如何使用泛型方法打印不同字符串的元素:
package cici; public class GenericMethodCiciTest { // 泛型方法 printArray public static < A > void printArray(A[] inputArray) { // 输出数组元素 for ( A element : inputArray ){ System.out.printf( "%s ", element); } System.out.println(); } public static void main(String args[]) { // 创建不同类型数组: Integer, Double 和 Character Integer[] intArray = {1, 2, 3, 4, 5}; Double[] doubleArray = {1.12, 2.23, 3.34, 4.46}; Character[] charArray = {'M', 'A', 'G', 'E', 'D', 'U'}; System.out.println("integerArray :"); printArray(intArray); // 传递一个整型数组 System.out.println("doubleArray :"); printArray(doubleArray); // 传递一个双精度型数组 System.out.println("characterArray :"); printArray(charArray); // 传递一个字符型型数组 } }
运行结果如下所示:
integerArray : 1 2 3 4 5 doubleArray : 1.12 2.23 3.34 4.46 characterArray : M A G E D U
泛型类
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。
和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
实例
如下实例演示了我们如何定义一个泛型类:
package cici; public class CiciTest<B> { private B t; public void add(B t) { this.t = t; } public B get() { return t; } public static void main(String[] args) { CiciTest<Integer> integerCiciTest = new CiciTest<Integer>(); CiciTest<String> stringCiciTest = new CiciTest<String>(); integerCiciTest.add(new Integer(10)); stringCiciTest.add(new String("Hello World")); System.out.printf("Integer Value :%d\n\n", integerCiciTest.get()); System.out.printf("String Value :%s\n", stringCiciTest.get()); } }
编译以上代码,运行结果如下所示:
Integer Value :10 String Value :Hello World
读写文件
FileInputStream 和 FileOutputStream。
FileInputStream
该流用于从文件读取数据,它的对象可以用关键字 new 来创建。
有多种构造方法可用来创建对象。
可以使用字符串类型的文件名来创建一个输入流对象来读取文件:
InputStream f = new FileInputStream("C:/java/hello");
也可以使用一个文件对象来创建一个输入流对象来读取文件。我们首先得使用 File() 方法来创建一个文件对象:
File f = new File("C:/java/hello"); InputStream in = new FileInputStream(f);
创建了InputStream对象,就可以使用下面的方法来读取流或者进行其他的流操作。
方法及描述:
public void close() throws IOException{} 关闭此文件输入流并释放与此流有关的所有系统资源。抛出IOException异常。 protected void finalize()throws IOException {} 这个方法清除与该文件的连接。确保在不再引用文件输入流时调用其 close 方法。抛出IOException异常。 public int read(int r)throws IOException{} 这个方法从 InputStream 对象读取指定字节的数据。返回为整数值。返回下一字节数据,如果已经到结尾则返回-1。 public int read(byte[] r) throws IOException{} 这个方法从输入流读取r.length长度的字节。返回读取的字节数。如果是文件结尾则返回-1。 public int available() throws IOException{} 返回下一次对此输入流调用的方法可以不受阻塞地从此输入流读取的字节数。返回一个整数值。
除了 InputStream 外,还有一些其他的输入流,更多的细节参考下面链接:
FileOutputStream
该类用来创建一个文件并向文件中写数据。
如果该流在打开文件进行输出前,目标文件不存在,那么该流会创建该文件。
有两个构造方法可以用来创建 FileOutputStream 对象。
使用字符串类型的文件名来创建一个输出流对象:
OutputStream f = new FileOutputStream("C:/java/hello")
也可以使用一个文件对象来创建一个输出流来写文件。我们首先得使用File()方法来创建一个文件对象:
File f = new File("C:/java/hello"); OutputStream fOut = new FileOutputStream(f);
创建OutputStream 对象完成后,就可以使用下面的方法来写入流或者进行其他的流操作。
方法及描述:
public void close() throws IOException{} 关闭此文件输入流并释放与此流有关的所有系统资源。抛出IOException异常。 protected void finalize()throws IOException {} 这个方法清除与该文件的连接。确保在不再引用文件输入流时调用其 close 方法。抛出IOException异常。 public void write(int w)throws IOException{} 这个方法把指定的字节写到输出流中。 public void write(byte[] w) 把指定数组中w.length长度的字节写到OutputStream中。
实例
下面是一个演示 InputStream 和 OutputStream 用法的例子:
ciStreamTest.java 文件代码:
package cici.file; import java.io.*; public class ciStreamTest { public static void main(String[] args) throws IOException { File f = new File("cici.txt"); // 构建FileOutputStream对象 FileOutputStream fos = new FileOutputStream(f); // 构建OutputStreamWriter对象,参数可以指定编码,这里指定为UTF-8 OutputStreamWriter writer = new OutputStreamWriter(fos, "UTF-8"); writer.append("请你输入"); writer.append("\r\n"); writer.append("cici"); // 关闭写入流 writer.close(); // 关闭输出流 fos.close(); // 构建FileInputStream对象 FileInputStream fip = new FileInputStream(f); // 构建InputStreamReader对象,编码与写入相同 InputStreamReader reader = new InputStreamReader(fip, "UTF-8"); StringBuffer sb = new StringBuffer(); while (reader.ready()) { sb.append((char) reader.read()); } System.out.println(sb.toString()); // 关闭读取流 reader.close(); // 关闭输入流,释放系统资源 fip.close(); } }
序列化和反序列化
序列化 是将某些对象转换为以后可以恢复的数据格式的过程。人们经常序列化对象以便将它们保存到存储中,或者作为通信的一部分发送。
反序列化 是该过程的逆过程,从某种格式获取结构化数据,并将其重建为对象。今天,用于序列化数据的最流行的数据格式是 JSON。
ObjectOutputStream
类的 writeObject()
方法可以实现序列化。 ObjectInputStream
类的 readObject()
方法用于反序列化。
示例代码
package codeAnalysis; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class readObjectTest { public static void main(String args[])throws Exception{ //定义obj对象 String obj="hello cici!"; //创建一个包含对象进行反序列化信息的”object”数据文件 FileOutputStream fos=new FileOutputStream("cici_object"); ObjectOutputStream os=new ObjectOutputStream(fos); //writeObject()方法将obj对象写入object文件 os.writeObject(obj); os.close(); //从文件中反序列化obj对象 FileInputStream fis=new FileInputStream("cici_object"); ObjectInputStream ois=new ObjectInputStream(fis); //恢复对象 String obj2=(String)ois.readObject(); System.out.print(obj2); ois.close(); } }
查看 cici_object 文件内容
这里需要注意的是, ac ed 00 05 是java序列化内容的特征,如果经过base64编码,那么相对应的是 rO0AB : echo aced0005 | xxd -r -ps | openssl base64
反序列化漏洞
对 类 进行序列化和反序列化
package codeAnalysis; import java.io.*; public class readObjectTest2 { public static void main(String args[])throws Exception{ //定义myObj对象 MyObject myObj = new MyObject(); myObj.name = "cici"; //创建一个包含对象进行反序列化信息的”object”数据文件 FileOutputStream fos = new FileOutputStream("cici_object2"); ObjectOutputStream os = new ObjectOutputStream(fos); //writeObject()方法将myObj对象写入object文件 os.writeObject(myObj); os.close(); //从文件中反序列化obj对象 FileInputStream fis = new FileInputStream("cici_object2"); ObjectInputStream ois = new ObjectInputStream(fis); //恢复对象 MyObject objectFromDisk = (MyObject)ois.readObject(); System.out.println(objectFromDisk.name); ois.close(); } } class MyObject implements Serializable { public String name; //重写readObject()方法 private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{ //执行默认的readObject()方法 in.defaultReadObject(); //执行打开计算器程序命令 Runtime.getRuntime().exec("open /Applications/Safari.app"); } }
可以看到 MyObject 类实现了 Serializable 结果,需要知道,只有实现了 Serializable 接口的类才可以被序列化。
下文会介绍fastjson漏洞
Spring 框架
Spring 是 Java EE 编程领域的一款轻量级的开源框架,由被称为“Spring 之父”的 Rod Johnson 于 2002 年提出并创立,它的目标就是要简化 Java 企业级应用程序的开发难度和周期。
Spring 自诞生以来备受青睐,一直被广大开发人员作为 Java 企业级应用程序开发的首选。时至今日,Spring 俨然成为了 Java EE 代名词,成为了构建 Java EE 应用的事实标准。
项目名称 描述 Spring Data Spring 提供的数据访问模块,对 JDBC 和 ORM 提供了很好的支持。通过它,开发人员可以使用一种相对统一的方式,来访问位于不同类型数据库中的数据。 Spring Batch 一款专门针对企业级系统中的日常批处理任务的轻量级框架,能够帮助开发人员方便的开发出健壮、高效的批处理应用程序。 Spring Security 前身为 Acegi,是 Spring 中较成熟的子模块之一。它是一款可以定制化的身份验证和访问控制框架。 Spring Mobile 是对 Spring MVC 的扩展,用来简化移动端 Web 应用的开发。 Spring Boot 是 Spring 团队提供的全新框架,它为 Spring 以及第三方库一些开箱即用的配置,可以简化Spring 应用的搭建及开发过程。 Spring Cloud 一款基于 Spring Boot 实现的微服务框架。它并不是某一门技术,而是一系列微服务解决方案或框架的有序集合。它将市面上成熟的、经过验证的微服务框架整合起来,并通过 Spring Boot 的思想进行再封装,屏蔽调其中复杂的配置和实现原理,最终为开发人员提供了一套简单易懂、易部署和易维护的分布式系统开发工具包。
Spring Framework 的特点
Spring 框架具有以下几个特点。
- 方便解耦,简化开发
Spring 就是一个大工厂,可以将所有对象的创建和依赖关系的维护交给 Spring 管理。 - 方便集成各种优秀框架
Spring 不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如 Struts2、Hibernate、MyBatis 等)的直接支持。 - 降低 Java EE API 的使用难度
Spring 对 Java EE 开发中非常难用的一些 API(JDBC、JavaMail、远程调用等)都提供了封装,使这些 API 应用的难度大大降低。 - 方便程序的测试
Spring 支持 JUnit4,可以通过注解方便地测试 Spring 程序。 - AOP 编程的支持
Spring 提供面向切面编程,可以方便地实现对程序进行权限拦截和运行监控等功能。 - 声明式事务的支持
只需要通过配置就可以完成对事务的管理,而无须手动编程。
创建一个SpringBoot程序
初始化
idea专业版新建项目,选择 Spring Initializr , 自定义名称之后,点击下一步
选择其中的 Spring Web
创建成功后,可以看到一个初始的spring项目架构
创建接口
接下来,我们使用Spring创建一个 HelloWorld 项目
package com.example.springdemo1; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @SpringBootApplication public class SpringDemo1Application { public static void main(String[] args) { SpringApplication.run(SpringDemo1Application.class, args); } @GetMapping("/hello") public String hello(@RequestParam(value = "name", defaultValue = "World") String name) { return String.format("Hello %s!", name); } }
hello() 我们添加的方法旨在采用名为的String参数 name ,然后将此参数与 "Hello" 代码中的单词组合。这意味着,如果您 “Amy” 在请求中将姓名设置为,则响应为 “Hello Amy” 。
该 @RestController 注解告诉Spring,这个代码描述应该可在⽹上的端点。该 @GetMapping(“/hello”) 告诉Spring使用我们的 hello() 方法来回答这个问题被发送到请求 http://localhost:8080/hello 的地址。最后, @RequestParam 告诉Spring期望 name 请求中的值,但是如果不存在,默认情况下它将使用单词“ World”。
访问接口 hello, 并给参数为 cici
http://127.0.0.1:8080/hello?name=cici
对接口层进行拆分
新建 controller 目录
在 UserController 文件中写入 接口逻辑
package com.example.springdemo1.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class UserController { @GetMapping("/hello") public String hello(@RequestParam(value = "name", defaultValue = "World") String name) { return String.format("Hello %s!", name); } }
入口文件
package com.example.springdemo1; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringDemo1Application { public static void main(String[] args) { SpringApplication.run(SpringDemo1Application.class, args); } }
替换接口注解
- 使用PostMapping注解
@RestController public class UserController { @PostMapping("/hello") public String hello(@RequestParam(value = "name", defaultValue = "World") String name) { return String.format("Hello %s!", name); } }
使用 @PostMapping 则只能使用 POST 方法,使用 GET 请求会报错
- 使用RequestMapping注解
@RestController public class UserController { @RequestMapping("/hello") public String hello(@RequestParam(value = "name", defaultValue = "World") String name) { return String.format("Hello %s!", name); } }
使用 RequestMapping 注解时,可同时接收 GET 和 POST 请求
安全分享
审计spring时
- 用户输入: 查看controller中的文件,检查注解下方法的参数
Java 代码审计
主要内容:
- SQL 注入
- 命令执行
- XSS
- XXE
- CSRF
- SSRF
- 重定向
- 上件上传
- Jsonp劫持
- 反序列化
- 自动化代码审计
- Cobra
- 什么是"源代码安全审计(白盒扫描)"
- Cobra 为什么能从源代码中扫描漏洞
- 什么是"源代码安全审计(白盒扫描)"
SQL 注入
安全威胁
SQL injection,SQL 注入攻击。
当应用程序将用户输入的内容,拼接到 SQL 语句中,一起提交给数据库执行时,就会产生 SQL 注入威胁。
由于用户的输入,也是 SQL 语句的一部分,所以攻击者可以利用这部分可以控制的内容,注入自己定义的语句,改变 SQL 语句执行逻辑,让数据库执行任意自己需要的指令。通过控制部分 SQL 语句,攻击者可以查询数据库中任何自己需要的数据,利用数据库的一些特性,可以直接获取数据库服务器的系统权限。
代码示例
只要支持 JDBC 查询,并且开发人员使用了语句拼接,都会产生这种漏洞。
JDBC
- 驱动类 : com.mysql.cj.jdbc.Driver。
- 连接网址 :
语法:
"jdbc:mysql://hostname:port/dbname","username","password"
MySql 数据库的连接 url : jdbc:mysql://localhost:3306/dvwa 其中 3306 是端口号,dvwa 是数据库名称。
- username :MySql数据库的用户名,默认为root。
- Password : MySql 数据库的密码。
代码连接前需下载 jdbc 驱动
MySQL数据库: https://dev.mysql.com/downloads/connector/j/
Java(jdbc)示例:
package codeAnalysis; import java.sql.Connection; import java.sql.DriverManager; /** * This class is used to create a JDBC * connection with MySql DB. * */ public class JDBCMySqlTest { //JDBC and database properties. private static final String DB_DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String DB_URL = "jdbc:mysql://127.0.0.1:33060/dvwa"; private static final String DB_USERNAME = "root"; private static final String DB_PASSWORD = ""; public static void main(String args[]){ Connection conn = null; try{ //Register the JDBC driver Class.forName(DB_DRIVER); //Open the connection conn = DriverManager. getConnection(DB_URL, DB_USERNAME, DB_PASSWORD); if(conn != null){ System.out.println("Successfully connected."); }else{ System.out.println("Failed to connect."); } }catch(Exception e){ e.printStackTrace(); } } }
JDBC 语句用于对数据库执行查询。语句是一个接口,它提供了执行查询的方法。我们可以通过调用 Connection接口的 createStatement() 方法得到一个语句对象。
Statement接口常用方法:
- execute(String SQL):用于执行SQL DDL语句。
public boolean execute(String SQL)
- executeQuery(String SQL):用于执行选择查询,返回一个ResultSet对象。
public ResultSet executeQuery(String SQL)
- executeUpdate(String SQL *) : *用于执行插入、更新、删除等查询,返回编号。受影响的行数。
public int executeUpdate(String SQL)
- executeBatch():用于批量执行命令。
public int[] executeBatch()
Statement statement = connection.createStatement(); String sql = "SELECT * FROM crawler_article"; ResultSet rs =statement.executeQuery(sql);
//如果@GetMapping("/hello")下方法参数是这样,就代表所有输入都在request中 HttpServletRequest request, HttpServletResponse response) { JdbcConnection conn = new JdbcConnection(); final String sql = "select \* from product where pname like '%" + request.getParameter("pname") + "%'"; conn.execqueryResultSet(sql);
JDBC使用预编译
public static void main(String[] args) { try { final String driverClassName = "com.mysql.cj.jdbc.Driver"; final String url = "jdbc:mysql://127.0.0.1:33060/dvwa"; final String username = "cici"; final String password = "123456"; Connection connection = DriverManager.getConnection(url2, username, password); String sql = " SELECT *\n" + " FROM cici\n" + " WHERE id = ? and name like ?"; Class.forName(driverClassName); PreparedStatement preparedStatement = connection.prepareStatement(sql); preparedStatement.setInt(1, 1); preparedStatement.setString(2, "%ing%"); ResultSet rst = preparedStatement.executeQuery(); rst.next(); System.out.println(rst.getString(2)); } catch (Exception e) { e.printStackTrace(); } }
Java(ibatis)示例 xml中写法
<select id="unsafe" resultMap="myResultMap"> select * from table where name like '%$value$%' </select> UnSafeBean b = sqlMap.queryForObject("value", request.getParameter("name"));
主流的 Java ORM 框架
当前 Java ORM 框架产品有很多,常见的框架有 Hibernate 和 MyBatis,其主要区别如下。
Hibernate
Hibernate 框架是一个全表映射的框架。通常开发者只要定义好持久化对象到数据库表的映射关系,就可以通过Hibernate 框架提供的方法完成持久层操作。
开发者并不需要熟练地掌握 SQL 语句的编写,Hibernate 框架会根据编制的存储逻辑,自动生成对应的 SQL,并调用 JDBC 接口来执行,所以其开发效率会高于 MyBatis 框架。
然而Hibernate框架自身也存在一些缺点,例如:
- 多表关联时,对 SQL 查询的支持较差;
- 更新数据时,需要发送所有字段;
- 不支持存储过程;
- 不能通过优化 SQL 来优化性能。
这些问题导致其只适合在场景不太复杂且对性能要求不高的项目中使用。
Hibernate 官网:http://hibernate.org/
Hibernate执行sql语句
usernameString//前台输入的用户名 passwordString//前台输入的密码 //hql语句 String queryString = "from User t where t.username= " + usernameString + " and t.password="+ passwordString; //执行查询 List result = session.createQuery(queryString).list(); //createQuery可以接收原生的sql语句
- Hibernate预编译
usernameString//前台输入的用户名 passwordString//前台输入的密码 //hql语句 String queryString = "from User t where t.username: usernameString and t.password: passwordString"; //执行查询 List result = session.createQuery(queryString) .setString("usernameString ", usernameString ) .setString("passwordString", passwordString) .list(); //hql语句 String queryString = "from User t where t.username=? and t.password=?"; //执行查询 List result = session.createQuery(queryString) .setString(0, usernameString ) .setString(1, passwordString) .list(); // like 查询 String query="from UserEntity where mobile like :mobile and name like :name"; Query queryObject = this.systemService.getSession().createQuery(query); queryObject.setParameter("mobile","%"+mobile+"%"); queryObject.setParameter("name","%"+name+"%" ); List<UserEntity> userlist = queryObject.list();
MyBatis
MyBatis 框架是一个半自动映射的框架。这里所谓的“半自动”是相对于 Hibernate 框架全表映射而言的,MyBatis框架需要手动匹配提供 POJO、SQL 和映射关系,而 Hibernate 框架只需提供 POJO 和映射关系即可。
与 Hibernate 框架相比,虽然使用 MyBatis 框架手动编写 SQL 要比使用 Hibernate 框架的工作量大,但 MyBatis框架可以配置动态 SQL 并优化 SQL、通过配置决定 SQL 的映射规则,以及支持存储过程等。对于一些复杂的和需要优化性能的项目来说,显然使用 MyBatis 框架更加合适。
MyBatis 框架可应用于需求多变的互联网项目,如电商项目;Hibernate 框架可应用于需求明确、业务固定的项目,如 OA 项目、ERP 项目等。
MyBatis 3 中文文档:https://mybatis.org/mybatis-3/zh/
- MyBatis 示例
MyBatis 使用 XML 定义语句的方式,MyBatis 提供的所有特性都可以利用基于 XML 的映射语言来实现。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.mybatis.example.BlogMapper"> <select id="selectBlog" resultType="Blog"> select * from Blog where id = ${id} # 拼接 </select> <select id="selectBlog" resultType="Blog"> select * from Blog where id = #{id} # 预编译 </select> </mapper>
// Mybatis like <select id="findUserByLikeName1" parameterType="java.lang.String" resultMap="user"> select * from t_user where name like '%${name}%' </select> // Mybatis like 使用预编译 select * from user where name like concat('%', #{name}, '%')
JdbcTemplate
Spring JDBC 提供了多个实用的数据库访问工具,以简化 JDBC 的开发,其中使用最多就是 JdbcTemplate。
JdbcTemplate 是 Spring JDBC 核心包(core)中的核心类,它可以通过配置文件、注解、Java 配置类等形式获取数据库的相关信息,实现了对 JDBC 开发过程中的驱动加载、连接的开启和关闭、SQL 语句的创建与执行、异常处理、事务处理、数据类型转换等操作的封装。我们只要对其传入SQL 语句和必要的参数即可轻松进行 JDBC 编程。
JdbcTemplate 的全限定命名为 org.springframework.jdbc.core.JdbcTemplate,它提供了大量的查询和更新数据库的方法,如下表所示。
方法 说明 public int update(String sql) 用于执行新增、更新、删除等语句;sql:需要执行的SQL 语句;args 表示需要传入到 SQL 语句中的参数。 public int update(String sql,Object... args) public void execute(String sql) 可以执行任意 SQL,一般用于执行 DDL 语句; sql:需要执行的 SQL 语句;action 表示执行完 SQL 语句后,要调用的函数。 public T execute(String sql,PreparedStatementCallback action) public List query(String sql, RowMapperrowMapper, @Nullable Object... args) 用于执行查询语句;sql:需要执行的 SQL 语句;rowMapper:用于确定返回的集合(List)的类型;args:表示需要传入到 SQL 语句的参数。 public T queryForObject(String sql,RowMapper rowMapper, @Nullable Object... args) public int[] batchUpdate(String sql,List<Object[]> batchArgs, final int[] argTypes) 用于批量执行新增、更新、删除等语句; sql:需要执行的 SQL 语句;argTypes:需要注入的 SQL 参数的 JDBC类型;batchArgs:表示需要传入到 SQL 语句的参数。
String sql1="select * from users where user = ? "; PreparedStatement preparedStatement1 = con.prepareStatement(sql1); preparedStatement1.setString(1, user); ResultSet re = preparedStatement1.executeQuery(); while (re.next()) { System.out.println(re.getString("user")); }
搜索与 Java 数据库相关的代码应该有助于查明被审计的应用程序的持久层中涉及的类和方法
要搜索的字符串
java.sql.Connection.prepareStatement java.sql.ResultSet.getObject select insert java.sql.Statement.executeUpdate java.sql.Statement.addBatch java.sql.Statement.executeQuery java.sql.Statement.execute execute executestatement createStatement java.sql.ResultSet.getString executeQuery jdbc delete update java.sql.Connection.prepareCall
解决方案
使用预处理执行 SQL 语句,对所有传入 SQL 语句中的变量,做绑定。这样,用户拼接进来的变量,无论内容是什么,都会被当做替代符号“?”所替代的值,数据库也不 会把恶意用户拼接进来的数据,当做部分 SQL 语句去解析。
示例:
com.mysql.jdbc.Connection conn = db.JdbcConnection.getConn(); final String sql = "select * from product where pname like ?"; java.sql.PreparedStatement ps = (java.sql.PreparedStatement) conn.prepareStatement(sql); ps.setObject(1, "%"+request.getParameter("pname")+"%"); ResultSet rs = ps.executeQuery();
无论使用了哪个 ORM 框架,都会支持用户自定义拼接语句,经常有人误解 Hibernate 没有这个漏洞,其实Hibernate 也支持用户执行 JDBC 查询,并且支持用户把变量拼接到SQL 语句中。
命令执行
安全威胁
Code injection,代码注入攻击
web 应用代码中,允许接收用户输入一段代码,之后在 web 应用服务器上执行这段代码,并返回给用户。
由于用户可以自定义输入一段代码,在服务器上执行,所以恶意用户可以写一个远 程控制木马,直接获取服务器控制权限,所有服务器上的资源都会被恶意用户获取和修 改,甚至可以直接控制数据库。
代码示例
Java执行系统命令函数
- java.lang.Runtime
- java.lang.ProcessBuilder
// java.lang.Runtime Process process = Runtime.getRuntime().exec(command); process.getOutputStream().close();
ProcessBuilder processBuilder = new ProcessBuilder(); processBuilder.command("bash", "-c", cmd); BufferedReader reader = new BufferedReader(new InputStreamReader(processBuilder.start().getInputStream(), StandardCharsets.UTF_8)); while ((line = reader.readLine()) != null) { System.out.println(line); } reader.close();
解决方案
不直接使用用户输入的参数传入命令执行函数中,或对传入的参数进行过滤,仅允许传入字母、数字、下划线等必要字符。(禁止传入|、&、;、&&、||、<、>等特殊字符)
文件上传
名称定义
File upload,任意文件上传攻击。
Web 应用程序在处理用户上传的文件时,没有判断文件的扩展名是否在允许的范围内,就把文件保存在服务器上,导致恶意用户可以上传任意文件,甚至上传脚本木马到web 服务器上,直接控制 web 服务器。
代码示例
读取文件函数:
getInputStream()/FileOutputStream() getOriginalFilename() FileWriter File 或查找 filename 相关的关键字进行检索;
处理用户上传文件请求的代码,这段代码没有过滤文件扩展名。
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter( request.getRealPath("/")+getFIlename(request)))); ServletInputStream in = request.getInputStream(); int i = in.read(); while (i != -1) { pw.print((char) i); i = in.read(); } pw.close();
@Controller public class UploadFile { @PostMapping("/upload") public String uploadFile(@RequestParam("uploadfile")MultipartFile file){ //获取文件名 String filename = file.getOriginalFilename(); //文件保存路径 String path="/var/www/html/cici/upload"; File outfile = new File(path + filename); try { file.transferTo(outfile); }catch (IOException e){ e.printStackTrace(); } return "success"; } }
审计点:
- 查看文件写入路径是否可以被用户控制
- 查看是否限制文件后缀
解决方案
处理用户上传文件,要做以下检查:
1、 检查上传文件扩展名白名单,不属于白名单内,不允许上传。
2、 上传文件的目录必须是 http 请求无法直接访问到的。如果需要访问的,必须上传到其他(和 web 服务器不同的)域名下,并设置该目录为不解析 jsp 等脚本语言的目录。
3、 上传文件要保存的文件名和目录名由系统根据时间生成,不允许用户自定义。
重定向
安全威胁
URL redirect,URL 跳转攻击。
Web 应用程序接收到用户提交的 URL 参数后,没有对参数做“可信任 URL”的验证, 就向用户浏览器返回跳转到该URL 的指令。
如果某个 web 应用程序存在这个漏洞,恶意攻击者可以发送给用户一个链接,但是用户打开后,却来到钓鱼网站页面,将会导致用户被钓鱼攻击,账号被盗,或账号相关财产被盗。
URL跳转相关函数:
sendRedirect() redirect() forward() setHeader()
代码示例
这是一段没有验证目的地址,就直接跳转的经典代码:
if(checklogin(request)){ response.sendRedirect(request.getParameter("url")); }
这段代码存在 URL 跳转漏洞,当用户登陆成功后,会跳转到 url 参数所指向的地址。
使用SpringMVC时使用 redirect 开头的字符串,可以起到重定向作用
public String redirect(){ return"redirect:http://www.baidu.com"; }
解决方案
为了保证用户所点击的 URL,是从 web 应用程序中生成的 URL,所以要做 TOKEN 验证。
如果应用只有跳转网站的需求,可以设置白名单,判断目的地址是 否在白名单列表中,如果不在列表中,就判定为URL 跳转攻击,并记录日志。不允许配置集团以外网站到白名单列表中。
这两个方案都可以保证所有在应用中发出的重定向地址,都是可信任的地址。
注意:设置白名单时要注意进行后缀匹配
CSRF
安全威胁
Cross-Site Request Forgery(CSRF),跨站请求伪造攻击。
攻击者在用户浏览网页时,利用页面元素(例如 img 的 src),强迫受害者的浏览器向 Web 应用程序发送一个改变用户信息的请求。
由于发生 CSRF 攻击后,攻击者是强迫用户向服务器发送请求,所以会造成用户信息被迫修改,更严重者引发蠕虫攻击。
CSRF 攻击可以从站外和站内发起。从站内发起 CSRF 攻击,需要利用网站本身的业务,比如“自定义头像”功能,恶意用户指定自己的头像 URL 是一个修改用户信息的链接,当其他已登录用户浏览恶意用户头像时,会自动向这个链接发送修改信息请求。
从站外发送请求,则需要恶意用户在自己的服务器上,放一个自动提交修改个人信 息的 htm 页面,并把页面地址发给受害者用户,受害者用户打开时,会发起一个请求。
如果恶意用户能够知道网站管理后台某项功能的 URL,就可以直接攻击管理员,强迫管理员执行恶意用户定义的操作。
代码示例
一个没有 CSRF 安全防御的代码如下:
public void csrfTest(HttpServletRequest request, HttpServletResponse response) { int userid=Integer.valueOf(request.getSession().getAttribute("userid").toString()); String email=request.getParameter("email"); String tel=request.getParameter("tel"); String ciciName=request.getParameter("student_name"); Object[] params = new Object[4]; params[0] = email; params[1] = tel; params[2] = realname; params[3] = userid; final String sql = "update user set email=?,tel=?,realname=? where userid=?"; conn.execUpdate(sql,params);
代码中接收用户提交的参数“email,tel,realname”,之后修改了该用户的数据,一 旦接收到一个用户发来的请求,就执行修改操作。
提交表单代码:
<form action="http://localhost/servlet/modify" method="POST"> <input name="email"> <input name="tel"> <input name="student_name"> <input name="userid"> <input type="submit"> </form>
当用户点提交时,就会触发修改操作。
审计时需要关注系统敏感功能逻辑,例如:增删改查、支付等
当是个敏感功能时要加csrf防御。
解决方案
- 在用户登陆时,设置一个 CSRF 的随机 TOKEN,同时种植在用户的 cookie 中, 当用户浏览器关闭、或用户再次登录、或退出时,清除 token。
- 在表单中,生成一个隐藏域,它的值就是 COOKIE 中随机 TOKEN。
- 表单被提交后,就可以在接收用户请求的 web 应用中,判断表单中的 TOKEN 值是否和用户 COOKIE 中的TOKEN 值一致,如果不一致或没有这个值,就判断为 CSRF 攻击,同时记录攻击日志(日志内容见“Error Handingand Logging” 章节)。
由于攻击者无法预测每一个用户登录时生成的那个随机 TOKEN 值,所以无法伪造这个参数。
示例:
<form method="post" id="xxxx" name="xxxx" style="margin:0px;"> $csrfToken.hiddenField ... </form>
代码中$csrfToken.hiddenField 将会生成一个隐藏域,用于生成验证 token,它将会作为表单的其提交。
- 验证referer
注意验证 referer 有可能会被绕过
同一网站中如果有其他漏洞配合或一些特殊情况下,也可以发出的 CSRF 攻击。
其他修复方案:加验证码、自定义请求头等
SSRF
SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种利用漏洞伪造服务器端发起请求。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。
安全威胁
通过控制功能中的发起请求的服务来当作跳板攻击内网中其他服务。比如,通过控制前台的请求远程地址加载的响应,来让请求数据由远程的URL域名修改为请求本地、或者内网的IP地址及服务,来造成对内网系统的攻击。
1 扫描内网开放服务
2 向内部任意主机的任意端口发送payload来攻击内网服务
3 DOS攻击(请求大文件,始终保持连接Keep-Alive Always)
4 攻击内网的web应用,例如直接SQL注入、XSS攻击等。内网的服务一般安全性较差。
5 利用file、gopher、dict协议读取本地文件、执行命令等
敏感函数:
Httpclient.execute() HttpURLConnection.connect() URL.openStream() URLConnection.openConnection()
示例代码
- Httpclient.execute() 函数
HttpGet httppost = new HttpGet(Geocode.url + "/" + ip); HttpClient httpclient = new DefaultHttpClient(); HttpResponse response = httpclient.execute(httppost);
- HttpURLConnection.connect() 函数
URL url = new URL("http://www.cici.com/login.do?username=admin&password=admin"); // 得到网络访问对象java.net.HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); // 连接 httpURLConnection.connect();
- URL.openStream() 函数
URL url = new URL("http://www.cici.com"); InputStream is = url.openStream(); InputStreamReader isr = new InputStreamReader(is,"utf-8"); BufferedReader br = new BufferedReader(isr); String data = br.readLine(); while(data != null){ System.out.println(data); data = br.readLine(); } br.close(); isr.close(); is.close();
- URLConnection.openConnection() 函数
URL realUrl = new URL(urlNameString); // 打开和URL之间的连接 URLConnection connection = realUrl.openConnection();
解决方案
漏洞修复代码
String[] urlwhitelist = {".cici.org", ".cici.com"}; public static Boolean ciciSSRFUrlCheck(String url, String[] urlwhitelist) { try { URL u = new URL(url); // 只允许http和https的协议通过 if (!u.getProtocol().startsWith("http") && !u.getProtocol().startsWith("https")) { return false; } // 获取域名,并转为小写 String host = u.getHost().toLowerCase(); for (String whiteurl: urlwhitelist){ if (host.endWith(whiteurl)) { return true; } } return false; } catch (Exception e) { return false; } }
1.域名白名单
2.过滤内网IP、不可信域名
3.禁用其他协议;
XSS
安全威胁
Cross Site Script(XSS),跨站脚本攻击。
攻击者利用应用程序的动态展示数据功能,在 html 页面里嵌入恶意代码。当用户浏览该页之时,这些嵌入在 html中的恶意代码会被执行,用户浏览器被攻击者控制,从而达到攻击者的特殊目的。
跨站脚本攻击有两种攻击形式
1、反射型跨站脚本攻击
攻击者会通过社会工程学手段,发送一个 URL 连接给用户打开,在用户打开页面的同时,浏览器会执行页面中嵌入的恶意脚本。
2、存储型跨站脚本攻击
攻击者利用 web 应用程序提供的录入或修改数据功能,将数据存储到服务器或用户cookie 中,当其他用户浏览展示该数据的页面时,浏览器会执行页面中嵌入的恶意脚本。所有浏览者都会受到攻击。
3、DOM 跨站攻击
由于 html 页面中,定义了一段 JS,根据用户的输入,显示一段 html 代码,攻击者可以在输入时,插入一段恶意脚本,最终展示时,会执行恶意脚本。
DOM 跨站和以上两个跨站攻击的差别是,DOM 跨站是纯页面脚本的输出,只有规范使用 JAVASCRIPT,才可以防御。
恶意攻击者可以利用跨站脚本攻击做到:
1、盗取用户 cookie,伪造用户身份登录。
2、控制用户浏览器。
3、结合浏览器及其插件漏洞,下载病毒木马到浏览者的计算机上执行。
4、衍生 URL 跳转漏洞。
5、让官方网站出现钓鱼页面。
6、蠕虫攻击
代码示例
直接在 html 页面展示“用户可控数据”,将直接导致跨站脚本威胁。
1.Java 示例: JSP 文件
while(rs.next()) { %> <tr> <td><%=rs.getInt("id") %></td> <td><%=rs.getString("pname")%></td> <td><%=rs.getString("pdesc")%></td> <td><%=rs.getString("ptype")%></td> </tr> <% }
代码中的几个变量被直接输出到了页面中,没有做任何安 全过滤,一旦让用户可以输入数据,都可能导致用户浏览器把“用户可控数据”当成JS脚本执行,或页面元素被“用户可控数据”插入的页面 HTML 代码控制,从而造成攻击。
- 直接返回用户输入的内容.
解决方案
HTML/XML 页面输出规范:
1.在 HTML/XML 中显示“用户可控数据”前,应该进行 html escape 转义。
JAVA 示例:
<div>#escapeHTML($user.name) </div> <td>#escapeHTML($user.name)</td> 所有 HTML 和 XML 中输出的数据,都应该做 html escape 转义。
2.在 javascript 内容中输出的“用户可控数据”,需要做 javascript escape 转义。
html 转义并不能保证在脚本执行区域内数据的安全,也不能保证脚本执行代码的正常运行。
JAVA 示例:
<script>alert('#escapeJavaScript($user.name)')</script> <script>x='#escapeJavaScript($user.name)'</script> <div onmouseover="x='#escapeJavaScript($user.name)'"</div>
3.在给用户设置认证 COOKIE 时,加入 HTTPONLY
js代码就访问不了cookie了
AJAX 输出规范:
1、XML 输出“用户可控数据”时,对数据部分做 HTML 转义。示例:
<?xml version="1.0" encoding="UTF-8" ?> <man> <name>**#xmlEscape($name)**</name> <man>
2、json 输出要先对变量内容中的“用户可控数据”单独作 htmlEscape,再对变量内容做一次 javascriptEscape。
String cityname=”浙江<B>”+StringUtil.htmlEscape(city.name)+”</B>”; String json = "citys:{city:['"+ StringUtil.javascriptEscape(cityname) + "']}";
3、非 xml 输出(包括 json、其他自定义数据格式),response 包中的 http 头的contentType,必须为 json,并且用户可控数据做 htmlEscape 后才能输出。
response.setContentType("application/json"); PrintWriter out = response.getWriter(); out.println(StringUtil.htmlEscape(ajaxReturn));
XXE
安全威胁
当开发人员配置其XML解析功能允许外部实体引用时,攻击者可利用其引发安全问题的配置方式,实施任意文件读取、内网端口探测、命令执行、拒绝服务等方面的攻击。
代码示例
xml示例
<!DOCTYPE foo [<!ELEMENT foo ANY >
<!ENTITY % xxe SYSTEM "http://xxx.xxx.xxx/cici.dtd" >
%xxe;]>
<foo>&xxe;</foo>
读取系统文件
<?xml version="1.0"?> <!DOCTYPE cici[ <!ENTITY f SYSTEM "file:///etc/passwd"> ]> <hhh>&f;</hhh>
Java 解析xml文件的方式
1.SAXReader
防止 Java org.dom4j.io.SAXReader 模块的XXE 漏洞
使用 saxReader 时,需设置以下三项才能保证防止XXE攻击
// 禁用doctype saxReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); // 禁用一般外部实体 saxReader.setFeature("http://xml.org/sax/features/external-general-entities", false); // 禁用外部实体参数 saxReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
2.XMLInputFactory(StAX 解析器)
// This disables DTDs entirely for that factory xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); // This causes XMLStreamException to be thrown if external DTDs are accessed. xmlInputFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); // disable external entities xmlInputFactory.setProperty("javax.xml.stream.isSupportingExternalEntities", false);
3.SAXBuilder
使用 Java org.jdom2.input.SAXBuilder 模块时防止XXE漏洞
SAXBuilder builder = new SAXBuilder(); builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); builder.setFeature("http://xml.org/sax/features/external-general-entities", false); builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false); builder.setExpandEntities(false); Document doc = builder.build(new File(fileName));
4.DocumentBuilder
DocumentBuilder builder = factory.newDocumentBuilder(); builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); builder.setFeature("http://xml.org/sax/features/external-general-entities", false); builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false); // Disable external DTDs as well builder.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd",false); Document d = builder.parse(data); NodeList sList = d.getElementsByTagName("student");
反序列化漏洞
Fastjson反序列化
漏洞描述
fastjson提供了autotype功能,在请求过程中,我们可以在请求包中通过修改 @type 的值,来反序列化为指定的类型,而fastjson在反序列化过程中会设置和获取类中的属性,如果类中存在恶意方法,就会导致代码执行这类问题。
测试代码
- 创建Maven项目
- 在 pom 文件中添加 fastjson依赖
xml文件如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>FastjsonDemo</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency> </dependencies> </project>
- 点击 Maven 同步依赖
- 先看一下 fastjson 的功能,可以将字符串反序列化成json
写一个 CiciUser 类,在set方法中添加执行命令的代码,命令内容为打开 Safari 浏览器
package cici.fastjsonDemo; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; public class Test { public static void main(String[] args) { String data = "{\"name\": \"cici\"}"; JSONObject cici_json = JSON.parseObject(data); System.out.println(cici_json.get("name")); System.out.println(cici_json); } } // 执行结果 // cici // {"name":"cici"}
从漏洞描述中我们知道,当fastjson进行反序列化时会设置类的属性,也就是会自动调用 get/set 方法,那么我们就可以写一个类来进行测试。
package exp; import java.io.IOException; public class CiciUser { private String name; private String age; public void setAge(String properties) throws IOException { System.out.println("setProperties is running..."); Runtime.getRuntime().exec("open /Applications/Safari.app"); this.age = properties; } public String getName() { System.out.println("getName is running ..."); return name; } public void setName(String name) { System.out.println("setName is running ..."); this.name = name; } }
新写一个fastjson解析函数,解析的字符串中 @type 内容为我们写好的类的路径 exp.CiciUser ,同时设置属性
package cici.fastjsonDemo; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import exp.CiciUser; public class Test { public static void main(String[] args) { String payload2 = "{\"@type\":\"exp.CiciUser\", \"name\":\"cici\",\"age\":\"\"}"; Object obj = JSON.parse(payload2); System.out.println(obj); } }
将 CiciUser 中的 Poc 改为 curl htpp://127.0.0.1:8888
package exp; import java.io.IOException; public class CiciUser { private String name; private String age; public void setAge(String properties) throws IOException { System.out.println("setProperties is running..."); Runtime.getRuntime().exec("curl http://127.0.0.1:8888"); this.age = properties; } public String getName() { System.out.println("getName is running ..."); return name; } public void setName(String name) { System.out.println("setName is running ..."); this.name = name; } }
用nc监听本地 8888 端口, 同时使用 curl 访问本地 8888 端口 ,可以看到nc收到HTTP请求信息
nc -l 8888
审计工具-Cobra
RIPS
- 无法识别框架,框架输入
- 无法自定义规则
- 数据流分析,无法跨文件
介绍
什么是“源代码安全审计(白盒扫描)”
由于开发人员的技术水平和安全意识各不相同,导致可能开发出一些存在安全漏洞的代码。 攻击者可以通过渗透测试来找到这些漏洞,从而导致应用被攻击、服务器被入侵、数据被下载、业务受到影响等等问题。 “源代码安全审计”是指通过审计发现源代码中的安全隐患和漏洞,而Cobra可将这个流程自动化。
现在也不维护了,但在开源里是个比较出名的工具。
Cobra为什么能从源代码中扫描到漏洞
对于一些特征较为明显的可以使用正则规则来直接进行匹配出,比如硬编码密码、错误的配置等。 对于OWASP Top 10的漏洞,Cobra通过预先梳理能造成危害的函数,并定位代码中所有出现该危害函数的地方,继而基于Lex(Lexical Analyzer Generator, 词法分析生成器)和Yacc(Yet Another Compiler-Compiler, 编译器代码生成器)将对应源代码解析为AST(Abstract Syntax Tree, 抽象语法树),分析危害函数的入参是否可控来判断是否存在漏洞(目前仅接入了PHP-AST,其它语言AST接入中)。
Cobra和其它源代码审计系统有什么区别或优势
Cobra定位是自动化发现源代码中大部分显著的安全问题,对于一些隐藏较深或特有的问题建议人工审计。
- 开发源代码(基于开放的MIT License,可更改源码)
- 支持开发语言多(支持十多种开发语言和文件类型)
- 支持漏洞类型多(支持数十种漏洞类型)
- 支持各种场景集成(提供API也可以命令行使用)
- 专业支持,持续维护(由白帽子、开发工程师和安全工程师一起持续维护更新,并在多家企业内部使用)
Cobra支持哪些开发语言
目前Cobra主要支持PHP、Java等主要开发语言及其它数十种文件类型,并持续更新规则和引擎以支持更多开发语言,具体见支持的开发语言和文件类型。
Cobra能发现哪些漏洞
覆盖大部分Web端常见漏洞和一些移动端(Android、iOS)通用漏洞,具体见支持的漏洞类型。
Installation(安装)
系统支持
mac OS 支持
Linux 支持
Windows 暂不支持
Kali安装python2 pip
使用kali系统自带的python2.7时,需要下载pip2.7;
1.更新apt源
vim /etc/apt/sources.list
2.下载pip2.7
# 下载pip2.7 wget --no-check-certificate 'https://bootstrap.pypa.io/pip/2.7/get-pip.py' python2 get-pip.py
安装方法
git clone https://github.com/WhaleShark-Team/cobra.git && cd cobra
pip install -r requirements.txt
python cobra.py --help
CLI模式
Examples(使用例子)
# 扫描一个文件夹的代码 $ python cobra.py -t tests/vulnerabilities # 扫描一个Git项目代码 $ python cobra.py -t https://github.com/FeeiCN/grw.git # 扫描一个文件夹,并将扫描结果导出为JSON文件 $ python cobra.py -t tests/vulnerabilities -f json -o /tmp/report.json # 扫描一个Git项目,并将扫描结果JSON文件推送到API上 $ python cobra.py -f json -o http://push.to.com/api -t https://github.com/FeeiCN/vc.git # 扫描一个Git项目,并将扫描结果JSON文件发送到邮箱中 $ python cobra.py -f json -o [email protected] -t https://github.com/FeeiCN/grw.git # 扫描一个文件夹代码的某两种漏洞 $ python cobra.py -t tests/vulnerabilities -r cvi-190001,cvi-190002 # 开启一个Cobra HTTP Server,然后可以使用API接口来添加扫描任务 $ python cobra.py -H 127.0.0.1 -P 8888 # 查看版本 $ python cobra.py --version # 查看帮助 $ python cobra.py --help # 扫描一个Git项目,扫描完毕自动删除缓存 $ python cobra.py -t http://github.com/xx/xx.git -dels # 扫描gitlab全部项目,配置好config中private_token,gitlab_url,cobra_ip $ python git_projects.py # 自动生成Cobra扫描周报发送至指定邮箱,需要配置好config中的SMTP服务器信息 $ python cobra.py -rp
Help(帮助)
└─# python2 cobra.py -h usage: cobra [-h] [-t <target>] [-f <format>] [-o <output>] [-r <rule_id>] [-d] [-sid SID] [-dels] [-rp] [-m] [-H <host>] [-P <port>] ,---. | | ,---.|---.,---.,---. | | || || ,---| `---``---``---`` `---^ v2.0.0-alpha.5 GitHub: https://github.com/WhaleShark-Team/cobra Cobra is a static code analysis system that automates the detecting vulnerabilities and security issue. optional arguments: -h, --help show this help message and exit Scan: -t <target>, --target <target> file, folder, compress, or repository address -f <format>, --format <format> vulnerability output format (formats: json, csv, xml) -o <output>, --output <output> vulnerability output STREAM, FILE, HTTP API URL, MAIL -r <rule_id>, --rule <rule_id> specifies rules e.g: CVI-100001,cvi-190001 -d, --debug open debug mode -sid SID, --sid SID scan id(API) -dels, --dels del target directory True or False -rp, --report automation report Cobra data -m, --md5 Create projects file md5 RESTful: -H <host>, --host <host> REST-JSON API Service Host -P <port>, --port <port> REST-JSON API Service Port Usage: python cobra.py -t tests/vulnerabilities python cobra.py -t tests/vulnerabilities -r cvi-190001,cvi-190002 python cobra.py -t tests/vulnerabilities -f json -o /tmp/report.json python cobra.py -t https://github.com/ethicalhack3r/DVWA -f json -o [email protected] python cobra.py -t https://github.com/ethicalhack3r/DVWA -f json -o http://push.to.com/api python cobra.py -H 127.0.0.1 -P 8888
API接口
添加扫描任务
- 请求接口
接口: /api/add 方法: POST 类型: JSON
- 请求参数
参数 类型 必填 描述 例子 key string 是 config 文件中配置的secret_key {"key":"your_secret_key"} target string或list 是 需要扫描的git地址,默认为master分支,如需指定分支或tag可在git地址末尾加上 :master 单个项目扫描: {"target": "https://github.com/FeeiCN/dict.git:master"} ; 多个项目扫描: {"target": ["https://github.com/FeeiCN/dict.git:master","https://github.com/FeeiCN/autossh.git:master"]} rule string 否 仅扫描指定规则,以,分隔 {"rule": "cvi-130003,cvi-130004"}
- 响应例子
{ "code": 1001, # 状态码为1001则表示逻辑处理正常 "result": { "msg": "Add scan job successfully.", # 消息 "sid": "a938e2y2vnkf", # 扫描的任务ID(调用任务状态查询时需要用到) "total_target_num": 1 # 扫描任务的项目总数 } }
查询扫描任务状态
- 请求接口
接口: /api/status 方法: POST 类型: JSON
- 请求参数
参数 类型 必填 描述 例子 key string 是 config 文件中配置的 secret_key {"key":"your_secret_key"} sid string 是 扫描的任务ID
- 响应例子
{ "code": 1001, # 状态码为1001则表示逻辑处理正常 "result": { "msg": "success", # 消息 "not_finished": 0, # 未完成的项目数 "report": "http://127.0.0.1/?sid=ae3ea90pkoo5", # 扫描报告页 "sid": "ae3ea90pkoo5", # 扫描的任务ID "allow_deploy": true, # 是否允许发布上线 "statistic": { # 高中低危漏洞数量 "high": 5, "medium": 18, "critical": 0, "low": 28 }, "status": "done", # 扫描状态 "still_running": {}, # 正在扫描的项目 "total_target_num": 1, # 扫描任务的项目总数 } }
完整的例子
启动HTTP服务
python cobra.py -H 127.0.0.1 -P 8888 # 启动之前需先生成 config 配置文件 # cp config.template config
添加扫描任务
# 添加一条任务 curl -H "Content-Type: application/json" -X POST http://127.0.0.1:8888/api/add -d '{"key":"your_secret_key","target":"https://github.com/FeeiCN/grw.git:master", "rule": "cvi-130003,cvi-130004"}' # 添加多条任务 curl -H "Content-Type: application/json" -X POST -d '{"key":"your_secret_key","target":["https://github.com/WhaleShark-Team/cobra.git:master","https://github.com/FeeiCN/grw.git:master"]}' http://127.0.0.1:8888/api/add
查询任务状态
curl -H "Content-Type: application/json" -X POST -d '{"key":"your_secret_key","sid":"a938e29vdse8"}' http://127.0.0.1:8888/api/status
Web 指定时间段漏洞统计
http://127.0.0.1:8888/report
Flow(规则编写流程)
- 编写规则文件CVI-XXXNNN.xml
参考[规则命名](http://cobra.feei.cn/rule_name)建立规则文件。 参考[规则模板](http://cobra.feei.cn/rule_template)和[规则样例](http://cobra.feei.cn/rule_demo)编写对应的规则、修复方案、测试用例等。
- 编写漏洞代码tests/vulnerabilities/v.language
编写实际可能出现的业务场景代码(只需编写一处即可)。
- 测试规则扫描python cobra.py -t tests/vulnerabilities/
测试扫描结果
Rule Template(规则模板)
<?xml version="1.0" encoding="UTF-8"?> <cobra document="https://github.com/WhaleShark-Team/cobra"> <name value="硬编码Token/Key"/> <language value="*"/> <match mode="regex-only-match"><![CDATA[(?![\d]{32})(?![a-fA-F]{32})([a-f\d]{32}|[A-F\d]{32})]]></match> <level value="2"/> <test> <case assert="true" remark="sha1"><![CDATA["41a6bc4d9a033e1627f448f0b9593f9316d071c1"]]></case> <case assert="true" remark="md5 lower"><![CDATA["d042343e49e40f16cb61bd203b0ce756"]]></case> <case assert="true" remark="md5 upper"><![CDATA[C787AFE9D9E86A6A6C78ACE99CA778EE]]></case> <case assert="false"><![CDATA[please like and subscribe to my]]></case> <case assert="false"><![CDATA[A32efC32c79823a2123AA8cbDDd3231c]]></case> <case assert="false"><![CDATA[ffffffffffffffffffffffffffffffff]]></case> <case assert="false"><![CDATA[01110101001110011101011010101001]]></case> <case assert="false"><![CDATA[00000000000000000000000000000000]]></case> </test> <solution> ## 安全风险 硬编码密码 ## 修复方案 将密码抽出统一放在配置文件中,配置文件不放在git中 </solution> <status value="on"/> <author name="Feei" email="[email protected]"/> </cobra>
规则字段规范
字段(英文) 字段(中文) 是否必填 类型 描述 例子 name 规则名称。是 string 描述规则名称 <name value="Logger敏感信息" /> language 规则语言 是 string 设置规则针对的开发语言,参见languages <language value="php" /> match 匹配规则1 是 string 匹配规则1 <match mode="regex-only-match"><![CDATA[regex content]]></match> match2 匹配规则2 否 string 匹配规则2 <match2 block="in-function-up"><![CDATA[regex content]]></match2> repair 修复规则 否 string匹配到此规则,则不算做漏洞 <repair block=""><![CDATA[regex content]]></repair> level 影响等级 是 integer 标记该规则扫到的漏洞危害等级,使用数字1-10。 <level value="3" /> solution 修复方案 是 string该规则扫描的漏洞对应的安全风险和修复方案 <solution>详细的安全风险和修复方案</solution> test 测试用例 是 case 该规则对应的测试用例 <test><case assert="true"><![CDATA[测试存在漏洞的代码]]> </case><case assert="false"><![CDATA[测试不存在漏洞的代码]]> </case></test> status 是否开启 是 boolean是否开启该规则的扫描,使用 on / off 来标记 <status value="1" /> author 规则作者 是 attr 规则作者的姓名和邮箱 <author name="Feei" email="[email protected]" />
核心字段<match>/<match2>/<repair>编写规范
<match> Mode( <match> 的规则模式)
用来描述规则类型,只能用在 <match> 中。
Mode 类型 默认模式 支持语言 描述 regex-only-match 正则仅匹配 是 * 默认是此模式,但需要显式的写在规则文件里。以正则的方式进行匹配,匹配到内容则算作漏洞 regex-param-controllable 正则参数可控 否 PHP/Java 以正则模式进行匹配,匹配出的变量可外部控制则为漏洞 function-param-controllable 函数参数可控 否 PHP 内容写函数名,将搜索所有该函数的调用,若参数外部可控则为漏洞。 find-extension 寻找指定后缀文件 否 * 找到指定后缀文件则算作漏洞
<match2> / <repair> Block( <match2> / <repair> 的匹配区块)
用来描述需要匹配的代码区块位置,只能用在 <match2> 或 <repair> 中。
区块 描述 in-current-line 由第一条规则触发的所在行 in-function 由第一条规则触发的函数体内 in-function-up 由第一条规则触发的所在行之上,所在函数体之内 in-function-down 由第一条规则触发的所在行之下,所在函数体之内 in-file 由第一条规则触发的文件内 in-file-up 由第一条规则触发的所在行之上,所在文件之内 in-file-down 由第一条规则触发的所在行之下,所在文件之内
Example
使用测试代码进行扫描
Web服务模式
更改配置文件
cp config.template config
修改 secret_key 为自己的key值
安装依赖
pip3 install -r requirements.txt
- 安装python3
yum install python3 # 安装pip wget https://bootstrap.pypa.io/get-pip.py --no-check-certificate python3 get-pip.py
使用以下命令启动Web服务
python3 cobra.py -H 0.0.0.0 -P 8888
- 扫描github上的代码
直接输入git地址即可(亦可扫描公司内部的Gitlab服务上的代码)
扫描完成后可以看到对应的扫描结果
- 扫描本地代码
使用文件上传的方式扫描本地代码(需将代码文件打包成压缩包进行上传)
- 使用web接口下发任务
使用postman下发任务
// 请求体 { "key": "cici_key", "target": "https://github.com/FeeiCN/dict.git:master", "rule": "cvi-130003" } // 响应 { "code": 1001, "result": { "msg": "Add scan job successfully.", "sid": "ab9149nhvcal", "total_target_num": 1 } }
查询任务状态
{ "key": "cici_key", "sid": "ab9149nhvcal" }
CLI模式
命令:
# 扫描文件 python3 cobra.py -t tests/vulnerabilities/v.java # 扫描目录 python3 cobra.py -t tests/vulnerabilities
使用全量规则对java文件进行扫描
使用 -f 与 -o 选项输出扫描结果
python3 cobra.py -t tests/vulnerabilities -f json -o /tmp/report2.json # 使用 -r 可指定规则 python3 cobra.py -t tests/vulnerabilities -r cvi-190001,cvi-190002
可以看到 json 格式的输出结果
自定义规则
先复制出一个规则模版
cp CVI-140002.xml CVI-1400022.xml
CVI-1400022.xml 规则文件
使用正则表达式匹配 select from 语句中有 + 拼接的行
<?xml version="1.0" encoding="UTF-8"?> <cobra document="https://github.com/WhaleShark-Team/cobra"> <name value="输出入参可能导致XSS"/> <language value="java"/> <match mode="regex-only-match"><![CDATA[select.*from.*\+.*]]></match> <level value="4"/> <solution> ## 安全风险 查找select sql 语句 ## 修复方案 参数化查询 </solution> <test> <case assert="true"><![CDATA[select user from users where id = 1]]></case> </test> <status value="on"/> <author name="Cici" email="[email protected]"/> </cobra>
扫描结果
json文件
{ "s6e97fel57zb":{ "extension":1, "file":1, "framework":"Unknown Framework", "language":"java", "push_rules":1, "target_directory":"/root/cobra/tests/vulnerabilities", "trigger_rules":1, "vulnerabilities":[ { "analysis":"REGEX-ONLY-MATCH(正则仅匹配+无修复规则)", "code_content":"String hql = \"select max(detailLineNo) from TWmsSoreturnAsnDetailEntity where isDel = 0 and asnId=\"+headId;", "commit_author":"Unknown", "commit_time":"Unknown", "file_path":"/v.java", "id":"140002", "language":"java", "level":"4", "line_number":"49", "match_result":null, "rule_name":"输出入参可能导致XSS", "solution":"## 安全风险\n 输出入参会导致XSS\n\n ## 修复方案\n使用Begis对参数进行过滤后再输出" } ], "target":"tests/vulnerabilities/v.java" } }
多匹配条件规则分析
查看CVI-200001.xml 规则文件
<?xml version="1.0" encoding="UTF-8"?> <cobra document="https://github.com/WhaleShark-Team/cobra"> <name value="不安全的随机数"/> <language value="java"/> <match mode="regex-only-match"><![CDATA[new Random\s*\(|Random\.next]]></match> <match2 block="in-file-up"><![CDATA[((java|scala)\.util\.Random)]]></match2> <level value="2"/>
查看第一条规则,使用 match 标签, mode 为 regex-only-match ,意为使用正则匹配
<match mode="regex-only-match"><![CDATA[new Random\s*\(|Random\.next]]></match>
查看第二条规则 match2 , block 为 in-file-up ,意为第二条规则匹配第一条规则所在文件行以上的内容
可匹配的Java代码为
import java.util.Random; // CVI-200001 String generateSecretToken() { Random r = new Random(); return Long.toHexString(r.nextLong()); }