赞
踩
学习网站:https://www.sxt.cn/Java_jQuery_in_action/History_Direction.html
刷题网站 牛客网专题-spring java sql
第一章 Java入门
计算机语言经历了三代:第一代是机器语言,第二代是汇编语言,第三代是高级语言
1.3.2 Java的核心优势–跨平台(JAVA虚拟机(JVM)是JAVA实现跨平台的核心)
1.3.3 Java各版本的含义
JavaSE(Java Standard Edition):标准版,定位在个人计算机上的应用
JavaEE(Java Enterprise Edition):企业版,定位在服务器端的应用。 JavaEE是JavaSE的扩展,增加了用于服务器开发的类库。如:JDBC
JavaME(Java Micro Edition):微型版,定位在消费性电子产品的应用上(注意:很多人开始会误解为安卓开发就是JavaME,这两个是完全不同的内容)
1.3.4 Java的特性和优势
跨平台/可移植性、 安全性、面向对象、简单性、高性能、分布式、多线程健壮性
1.3.5 Java应用程序的运行机制
计算机高级语言的类型主要有编译型和解释型两种,而Java 语言是两种类型的结合。
Java首先利用文本编辑器编写 Java源程序,源文件的后缀名为.java;再利用编译器(javac)将源程序编译成字节码文件,字节码文件的后缀名为.class; 最后利用虚拟机(解释器,java)解释执行。
1.3.6 JVM、JRE和JDK
JVM(Java Virtual Machine)就是一个虚拟的用于执行bytecode字节码的”虚拟计算机”。他也定义了指令集、寄存器集、结构栈、垃圾收集堆、内存区域。JVM负责将Java字节码解释运行,边解释边运行,这样,速度就会受到一定的影响。
不同的操作系统有不同的虚拟机。Java 虚拟机机制屏蔽了底层运行平台的差别,实现了“一次编译,随处运行”。 Java虚拟机是实现跨平台的核心机制。如图1-6所示。
Java Runtime Environment (JRE) 包含:Java虚拟机、库函数、运行Java应用程序所必须的文件。
Java Development Kit (JDK)包含:包含JRE,以及增加编译器和调试器等用于程序开发的文件。
JDK、JRE和JVM的关系如图1-7所示。
老鸟建议:
·如果只是要运行Java程序,只需要JRE就可以。JRE通常非常小,其中包含了JVM。
·如果要开发Java程序,就需要安装JDK。
1.5.1 开发第一个Java程序
·使用记事本,编写代码
【示例1-1】使用记事本开发第一个Java程序
1
2
3
4
5 public class Welcome{
public static void main(String[] args){
System.out.println(“Hello Java!我是尚学堂学员,程许愿”);
}
}
可在d盘下建立文件夹mycode,用于保存学习的代码。保存路径建议为:”d:/mycode”。保存为:Welcome.java (文件名必须为Welcome,大小写也必须一致)。如图1-19所示。
菜鸟雷区
1.代码中的引号、分号必须为英文引号和分号,不能是中文全角的引号和分号
2.注意大小写
·编译(编译器创建class字节码文件)
打开命令行窗口,进入Java文件所在目录;执行命令:javac Welcome.java,生成class文件。
上图编译Java源文件
·解释并运行阶段
执行:”java Welcome”(就是运行编译生成的Welcome.class文件),输出执行结果。
上图解释并运行程序
1. 修改完java源文件,一定要重新编译才能生效;
1.5.3 第一个Java程序的总结和提升
1.Java对大小写敏感,如果出现了大小写拼写错误,程序无法运行。
2.关键字public被称作访问修饰符(access modifier),用于控制程序的其它部分对这段代码的访问级别。
3.关键字class 的意思是类。Java是面向对象的语言,所有代码必须位于类里面。
4.一个源文件中至多只能声明一个public的类,其它类的个数不限,如果源文件中包含一个public 类,源文件名必须和其中定义的public的类名相同,且以“.java”为扩展名。
5.一个源文件可以包含多个类class。
6.正确编译后的源文件,会得到相应的字节码文件,编译器为每个类生成独立的字节码文件,且将字节码文件自动命名为类的名字且以“.class”为扩展名。
7.main方法是Java应用程序的入口方法,它有固定的书写格式:
8.public static void main(String[] args) {…}
9.在Java中,用花括号划分程序的各个部分,任何方法的代码都必须以“{”开始,以“}”结束, 由于编译器忽略空格,所以花括号风格不受限制。
10.Java中每个语句必须以分号结束,回车不是语句的结束标志,所以一个语句可以跨多行。
老鸟建议:
·编程时,一定要注意缩进规范;
·在写括号、引号时,一定是成对编写,然后再往里插入内容。
1.5.4 最常用DOS命令
1.cd 目录路径 进入一个目录
2.cd … 进入父目录
3.dir 查看本目录下的文件和子目录列表
4.cls 清楚屏幕命令
5.上下键 查找敲过的命令
6.Tab键 自动补齐命令
第一章 总结
1.所有的编程语言的最终目的都是提供一种“抽象”方法。抽象的层次越高,越接近人的思维。越接近人的思维,越容易使用。
2.越高级的语言越容易学习;当然,这只意味着容易入门;不意味着成为高手越容易,高手仍然需要修炼。
3.Java的核心优势:跨平台。跨平台是靠JVM(虚拟机)实现的。
4.Java各版本的含义:
JavaSE(Java Standard Edition)标准版,定位在个人计算机的应用。
JavaEE(Java Enterprise Edition)企业版,定位在服务器端的应用。
JavaME(Java Micro Edition)微型版,定位在消费电子产品的应用。
5.Java程序的开发运行过程为:编写代码、编译、解释运行。
6.JDK用于开发Java程序,JRE是Java运行环境; JVM是JRE的子集,JRE是JDK的子集。
7.JDK配置,需要新建JAVA_HOME环境变量;需要修改Path环境变量。
8.Java是面向对象的语言,所有代码必须位于类里面。main方法是Java应用程序的入口方法。
9.常见的Java集成开发环境有三个:eclipse、IntelliJ IDE、NetBeans。
1、Java虚拟机就是一个虚拟的用于执行 字节码文件 的计算机。它是Java最核心的技术,是Java跨平台的基础。
2、一个Java源程序是由若干个类组成。如果源文件中有多个类时,则只能有一个类是
Public 类,并且这个类必须与源文件名同名。
第一章 作业
一、选择题
第二章 数据类型和运算
2.1 注释
单行注释、多行注释和文档注释。
单行注释: 使用“//”开头,“//”后面的单行内容均为注释。
多行注释: 以“/”开头以“/”结尾,在“/”和“/”之间的内容为注释,我们也可以使用多行注释作为行内注释。但是在使用时要注意,多行注释不能嵌套使用。
文档注释: 以“/”开头以“*/”结尾,注释中包含一些说明性的文字及一些JavaDoc标签(后期写项目时,可以生成项目的API)
【示例2-1】认识Java的三种注释类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 /
2.2 标识符
标识符是用来给变量、类、方法以及包进行命名的,如Welcome、main、System、age、name、gender等。
标识符需要遵守一定的规则:
标识符必须以字母、下划线_、美元符号
开头。标识符其它部分可以是字母、下划线
“
”
、美元符“
开头。 标识符其它部分可以是字母、下划线“_”、美元符“
开头。标识符其它部分可以是字母、下划线“”、美元符“”和数字的任意组合。
Java 标识符大小写敏感,且长度无限制。
标识符不可以是Java的关键字。
标识符的使用规范
表示类名的标识符:每个单词的首字母大写,如Man, GoodMan
表示方法和变量的标识符:第一个单词小写,从第二个单词开始首字母大写,我们称之为“驼峰原则”,如eat(), eatFood()
【注意】:Java不采用通常语言使用的ASCII字符集,而是采用Unicode这样标准的国际字符集。因此,这里字母的含义不仅仅是英文,还包括汉字等等。但是不建议大家使用汉字来定义标识符!
【示例2-2】合法的标识符
1
2
3
4 int a = 3;
int _123 = 3;
int $12aa = 3;
int 变量1 = 55; //合法,但是不建议使用中文命名的标识符
【示例2-3】不合法的标识符
1
2
3 int 1a = 3; //不能用数字开头
int a# = 3; //不能包含#这样的特殊字符
int int = 3; //不能使用关键字
课堂测试代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 /**
*/
public class TestIdentifer {
//能力是练出来的,不是看书看出来的。对于初学者来说,再简单的代码也一定要敲一下!
public static void main(String[] args) {
int a123 = 1;
//int 123abc = 2; //数字不能开头
int $a = 3;
int _abc = 4;
//int #abc = 5;
int 年龄 = 18; //可以使用汉字,但是一般不建议
//int class = 2; //关键字不能作为标识符
}
}
2.3 Java中的关键字/保留字
Java关键字是Java语言保留供内部使用的,如class用于定义类。 关键字也可以称为保留字,它们的意思是一样的,我们不能使用关键字作为变量名或方法名。
表2-1 Java中的关键字/保留字
abstract assert boolean break byte case
catch char class const continue default
do double else extends final finally
float for goto if implements import
instanceof int interface long native new
null package private protected public return
short static strictfp super switch synchronized
this throw throws transient try void
volatile while
菜鸟雷区
出于应试教育的惯性思维,很多新手很可能去背上面的单词,从实战思维出发,我们不需要刻意去记!随着学习的深入,自然就非常熟悉了。
2.4.1 变量的本质
变量本质上就是代表一个”可操作的存储空间”,空间位置是确定的,但是里面放置什么值不确定。我们可通过变量名来访问“对应的存储空间”,从而操纵这个“存储空间”存储的值。
Java是一种强类型语言,每个变量都必须声明其数据类型。变量的数据类型决定了变量占据存储空间的大小。 比如,int a=3; 表示a变量的空间大小为4个字节。
变量作为程序中最基本的存储单元,其要素包括变量名,变量类型和作用域。变量在使用前必须对其声明, 只有在变量声明以后,才能为其分配相应长度的存储空间。
·变量的声明
格式为:
1
2
3 type varName [=value][,varName[=value]…];
//[]中的内容为可选项,即可有可无
数据类型 变量名 [=初始值] [,变量名 [=初始值]…];
【示例2-4】 声明变量:
1
2
3 double salary;
long earthPopulation;
int age;
不同数据类型的常量会在内存中分配不同的空间,如图2-1所示。
图2-1 声明变量的内存示意图
注意事项
每个变量都有类型,类型可以是基本类型,也可以是引用类型。
变量名必须是合法的标识符
变量声明是一条完整的语句,因此每一个声明都必须以分号结束
【示例2-5】在一行中声明多个变量
1 int i ,j; // 两个变量的数据类型都是int
老鸟建议
不提倡这种"一行声明多个变量"风格,逐一声明每一个变量可以提高程序可读性。
【示例2-6】可以将变量的声明和初始化放在同一行中
1
2 int age = 18;
double e = 2.718281828;
2.4.2 变量的分类
从整体上可将变量划分为局部变量、成员变量(也称为实例变量)和静态变量。
表2-2局部变量、成员变量、静态变量的区别
类型 声明位置 从属于 生命周期
局部变量 方法或语句块内部 方法/语句块 从声明位置开始,直到方法或语句块执行完毕,局部变量消失
成员变量
(实例变量) 类内部,方法外部 对象 对象创建,成员变量也跟着创建。对象消失,成员变量也跟着消失;
静态变量
(类变量) 类内部,static修饰 类 类被加载,静态变量就有效;类被卸载,静态变量消失。
老鸟建议
成员变量和静态变量不是目前重点,不要过多纠结理解与否。我们学习面向对象时,再重点讲解成员变量和静态变量
· 局部变量(local variable)
方法或语句块内部定义的变量。生命周期是从声明位置开始到到方法或语句块执行完毕为止。局部变量在使用前必须先声明、初始化(赋初值)再使用。
【示例2-7】局部变量
1
2
3
4
5
6
7
8
9
10 public void test() {
int i;
int j = i+5 ; // 编译出错,变量i还未被初始化
}
public void test() {
int i;
i=10;
int j = i+5 ; // 编译正确
}
· 成员变量(也叫实例变量 member variable)
方法外部、类的内部定义的变量。从属于对象,生命周期伴随对象始终。如果不自行初始化,它会自动初始化成该类型的默认初始值。
表2-3实例变量的默认初始值
数据类型 实始值
int 0
double 0.0
char ‘\u0000’
boolean false
【示例2-8】实例变量的声明
1
2
3 public class Test {
int i;
}
· 静态变量(类变量 static variable)
使用static定义。 从属于类,生命周期伴随类始终,从类加载到卸载。 (注:讲完内存分析后我们再深入!先放一放这个概念!)如果不自行初始化,与成员变量相同会自动初始化成该类型的默认初始值,如表 2-3所示。
课堂练习1:变量的声明并赋值
1
2
3
4
5
6
7
8
9
10
11
12
13 public class LocalVariableTest {
public static void main(String[ ] args) {
boolean flag = true; // 声明boolean型变量并赋值
char c1, c2; // 声明char型变量
c1 = ‘\u0041’; // 为char型变量赋值
c2 = ‘B’; // 为char型变量赋值
int x; // 声明int型变量
x = 9; //为int型变量赋值
int y = x; // 声明并初始化int型变量
float f = 3.15f; // 声明float型变量并赋值
double d = 3.1415926; //声明double型变量并赋值
}
}
课堂代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 /**
*/
public class TestVariable {
int a; //成员变量, 从属于对象; 成员变量会自动被初始化 static int size; //静态变量,从属于类 public static void main(String[] args) { { int age; //局部变量,从属于语句块; age = 18; } int salary = 3000; //局部变量,从属于方法 int gao = 13; System.out.println(gao); int i; // int j = i + 5; // 编译出错,变量i还未被初始化 }
}
2.5 常量(Constant)
常量通常指的是一个固定的值,例如:1、2、3、’a’、’b’、true、false、”helloWorld”等。
在Java语言中,主要是利用关键字final来定义一个常量。 常量一旦被初始化后不能再更改其值。
声明格式为:
1 final type varName = value;
【示例2-9】常量的声明及使用
1
2
3
4
5
6
7
8
9
10
11 public class TestConstants {
public static void main(String[] args) {
final double PI = 3.14;
// PI = 3.15; //编译错误,不能再被赋值!
double r = 4;
double area = PI * r * r;
double circle = 2 * PI * r;
System.out.println("area = " + area);
System.out.println("circle = " + circle);
}
}
为了更好的区分和表述,一般将1、2、3、’a’、’b’、true、false、”helloWorld”等称为字面常量,而使用final修饰的PI等称为符号常量。
老鸟建议
变量和常量命名规范(规范是程序员的基本准则,不规范会直接损害你的个人形象):
所有变量、方法、类名:见名知意
类成员变量:首字母小写和驼峰原则: monthSalary
局部变量:首字母小写和驼峰原则
常量:大写字母和下划线:MAX_VALUE
类名:首字母大写和驼峰原则: Man, GoodMan
方法名:首字母小写和驼峰原则: run(), runRun()
2.6 基本数据类型(primitive data type)
Java是一种强类型语言,每个变量都必须声明其数据类型。 Java的数据类型可分为两大类:基本数据类型(primitive data type)和引用数据类型(reference data type)。
Java中定义了3类8种基本数据类型
数值型- byte、 short、int、 long、float、 double
字符型- char
布尔型-boolean
图2-2 数据类型的分类
注意事项
引用数据类型的大小统一为4个字节,记录的是其引用对象的地址!
本章只讲解基本数据类型。引用数据类型在后续数组和面向对象章节讲解。
2.6.1 整型变量/常量
整型用于表示没有小数部分的数值,它允许是负数。整型的范围与运行Java代码的机器无关,这正是Java程序具有很强移植能力的原因之一。与此相反,C和C++程序需要针对不同的处理器选择最有效的整型。
表2-4整型数据类型
类型 占用存储空间 表数范围
byte 1字节 -27 ~ 27-1(-128~127)
short 2字节 -215 ~ 215-1(-32768~32767)
int 4字节 -231 ~ 231-1 (-2147483648~2147483647)约21亿
long 8字节 -263 ~ 263-1
Java 语言整型常量的四种表示形式
十进制整数,如:99, -500, 0
八进制整数,要求以 0 开头,如:015
十六进制数,要求 0x 或 0X 开头,如:0x15
二进制数,要求0b或0B开头,如:0b01110011
Java语言的整型常数默认为int型,声明long型常量可以后加‘ l ’或‘ L ’ 。
【示例2-10】长整型常数的声明
1
2 long a = 55555555; //编译成功,在int表示的范围内(21亿内)。
long b = 55555555555;//不加L编译错误,已经超过int表示的范围。
我们修改成long类型的常量即可:
1 long b = 55555555555L;
2.6.2 浮点型变量/常量
带小数的数据在Java中称为浮点型。浮点型可分为float类型和double类型。
表2-5浮点型数据类型
类型 占用存储空间 表数范围
float 4字节 -3.403E38~3.403E38
double 8字节 -1.798E308~1.798E308
float类型又被称作单精度类型,尾数可以精确到7位有效数字,在很多情况下,float类型的精度很难满足需求。而double表示这种类型的数值精度约是float类型的两倍,又被称作双精度类型,绝大部分应用程序都采用double类型。浮点型常量默认类型也是double。
Java浮点类型常量有两种表示形
十进制数形式,例如:3.14 314.0 0.314
科学记数法形式,如314e2 314E2 314E-2
【示例2-11】使用科学记数法给浮点型变量赋值
1
2 double f = 314e2; //31410^2–>31400.0
double f2 = 314e-2; //31410^(-2)–>3.14
float类型的数值有一个后缀F或者f ,没有后缀F/f的浮点数值默认为double类型。也可以在浮点数值后添加后缀D或者d, 以明确其为double类型。
【示例2-12】float类型赋值时需要添加后缀F/f
1
2
3 float f = 3.14F;
double d1 = 3.14;
double d2 = 3.14D;
老鸟建议
浮点类型float,double的数据不适合在不容许舍入误差的金融计算领域。如果需要进行不产生舍入误差的精确数字计算,需要使用BigDecimal类。
【示例2-13】浮点数的比较一
1
2
3 float f = 0.1f;
double d = 1.0/10;
System.out.println(fd);//结果为false
【示例2-14】浮点数的比较二
1
2
3
4
5
6
7 float d1 = 423432423f;
float d2 = d1+1;
if(d1d2){
System.out.println("d1d2");//输出结果为d1d2
}else{
System.out.println(“d1!=d2”);
}
运行以上两个示例,发现示例2-13的结果是“false”,而示例2-14的输出结果是“d1==d2”。这是因为由于字长有限,浮点数能够精确表示的数是有限的,因而也是离散的。 浮点数一般都存在舍入误差,很多数字无法精确表示(例如0.1),其结果只能是接近, 但不等于。二进制浮点数不能精确的表示0.1、0.01、0.001这样10的负次幂。并不是所有的小数都能可以精确的用二进制浮点数表示。
java.math包下面的两个有用的类:BigInteger和BigDecimal,这两个类可以处理任意长度的数值。BigInteger实现了任意精度的整数运算。BigDecimal实现了任意精度的浮点运算。
菜鸟雷区
1.不要使用浮点数进行比较!很多新人甚至很多理论不扎实的有工作经验的程序员也会犯这个错误!需要比较请使用BigDecimal类
【示例2-15】使用BigDecimal进行浮点数的比较
1
2
3
4
5
6
7
8
9
10
11
12
13 import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
BigDecimal bd = BigDecimal.valueOf(1.0);
bd = bd.subtract(BigDecimal.valueOf(0.1));
bd = bd.subtract(BigDecimal.valueOf(0.1));
bd = bd.subtract(BigDecimal.valueOf(0.1));
bd = bd.subtract(BigDecimal.valueOf(0.1));
bd = bd.subtract(BigDecimal.valueOf(0.1));
System.out.println(bd);//0.5
System.out.println(1.0 - 0.1 - 0.1 - 0.1 - 0.1 - 0.1);//0.5000000000000001
}
}
浮点数使用总结
默认是double类型
浮点数存在舍入误差,数字不能精确表示。如果需要进行不产生舍入误差的精确数字计算,需要使用BigDecimal类。
避免比较中使用浮点数,需要比较请使用BigDecimal类
2.6.3 字符型变量/常量
字符型在内存中占2个字节,在Java中使用单引号来表示字符常量。
例如’A’是一个字符,它与”A”是不同的,”A”表示含有一个字符的字符串。
char 类型用来表示在Unicode编码表中的字符。Unicode编码被设计用来处理各种语言的文字,它占2个字节,可允许有65536个字符。
【示例2-16】字符型举例
char eChar = ‘a’;
char cChar =‘中’;
Unicode具有从0到65535之间的编码,他们通常用从’\u0000’到’\uFFFF’之间的十六进制值来表示(前缀为u表示Unicode)
【示例2-17】字符型的十六进制值表示方法
char c = ‘\u0061’;
Java 语言中还允许使用转义字符 ‘\’ 来将其后的字符转变为其它的含义。常用的转义字符及其含义和Unicode值如表2-6所示。
【示例2-18】转义字符
char c2 = ‘\n’; //代表换行符
表2-6转义字符
转义符 含义 Unicode值
\b 退格(backspace) \u0008
\n 换行 \u000a
\r 回车 \u000d
\t 制表符(tab) \u0009
\“ 双引号 \u0022
\‘ 单引号 \u0027
\ 反斜杠 \u005c
注意事项
以后我们学的String类,其实是字符序列(char sequence)。
课堂代码
/**
*/
public class TestPrimitiveDataType3 {
public static void main(String[] args) {
char a = ‘T’;
char b = ‘尚’;
char c = ‘\u0061’;
System.out.println©;
//转义字符
System.out.println(""+'a'+'\n'+'b');
System.out.println(""+'a'+'\t'+'b');
System.out.println(""+'a'+'\''+'b'); //a'b
//String就是字符序列
String d = "abc";
}
}
2.6.4 boolean类型变量/常量
boolean类型有两个常量值,true和false,在内存中占一位(不是一个字节),不可以使用 0 或非 0 的整数替代 true 和 false ,这点和C语言不同。 boolean 类型用来判断逻辑条件,一般用于程序流程控制 。
【示例2-19】boolean类型
boolean flag ;
flag = true; //或者flag=false;
if(flag) {
// true分支
} else {
// false分支
}
老鸟建议
Less is More!!请不要这样写:if ( flag == true ),只有新手才那么写。关键也很容易写错成if(flag=true),这样就变成赋值flag 为true而不是判断!老鸟的写法是if ( flag )或者if ( !flag)
2.7 运算符(operator)
计算机的最基本用途之一就是执行数学运算,作为一门计算机语言,Java也提供了一套丰富的运算符来操作变量。
表2-7 运算符分类
算术运算符 二元运算符 +,-,,/,%
一元运算符 ++,–
赋值运算符 =
扩展运算符 +=,-=,=,/=
关系运算符 >,<,>=,<=,==,!= instanceof
逻辑运算符 &&,||,!,^
位运算符 &,|,^,~ , >>,<<,>>>
条件运算符 ? :
字符串连接符 +
2.7.1 算术运算符
算术运算符中+,-,*,/,%属于二元运算符,二元运算符指的是需要两个操作数才能完成运算的运算符。其中的%是取模运算符,就是我们常说的求余数操作。
二元运算符的运算规则:
整数运算:
1. 如果两个操作数有一个为Long, 则结果也为long。
2. 没有long时,结果为int。即使操作数全为short,byte,结果也是int。
浮点运算:
3. 如果两个操作数有一个为double,则结果为double。
4. 只有两个操作数都是float,则结果才为float。
取模运算:
1.其操作数可以为浮点数,一般使用整数,结果是“余数”,“余数”符号和左边操作数相同,如:7%3=1,-7%3=-1,7%-3=1。
算术运算符中++,–属于一元运算符,该类运算符只需要一个操作数。
【示例2-20】一元运算符++与–
1
2
3
4
5
6 int a = 3;
int b = a++; //执行完后,b=3。先给b赋值,再自增。
System.out.println(“a=”+a+“\nb=”+b);
a = 3;
b = ++a; //执行完后,c=5。a先自增,再给c赋值
System.out.println(“a=”+a+“\nb=”+b);
运行该程序,执行结果如图2-3所示。
图2-3 示例2-20运行效果图
2.7.2 赋值及其扩展赋值运算符
表2-8 赋值及其扩展运算符
运算符 用法举例 等效的表达式
+= a += b a = a+b
-= a -= b a = a-b
*= a = b a = ab
/= a = b a = a/b
%= a = b a = a%b
【示例2-21】扩展运算符
1
2
3
4
5
6
7 int a=3;
int b=4;
a+=b;//相当于a=a+b;
System.out.println(“a=”+a+“\nb=”+b);
a=3;
a=b+3;//相当于a=a(b+3)
System.out.println(“a=”+a+“\nb=”+b);
运行该程序,执行结果如图2-4所示。
图2-4 示例2-21运行效果图
2.7.3 关系运算符
关系运算符用来进行比较运算,如表2-9所示。关系运算的结果是布尔值:true/false;
表2-9 关系运算符
运算符 含义 示例
== 等于 a==b
!= 不等于 a!=b
大于 a>b
< 小于 a<b
= 大于或等于 a>=b
<= 小于或等于 a<=b
注意事项
=是赋值运算符,而真正的判断两个操作数是否相等的运算符是==。
==、!= 是所有(基本和引用)数据类型都可以使用
、>=、 <、 <= 仅针对数值类型(byte/short/int/long, float/double。以及char)
2.7.4 逻辑运算符
Java中的逻辑运算符如表2-10所示。逻辑运算的操作数和运算结果都是boolean值。
表2-10 逻辑运算符
运算符 说明
逻辑与 &( 与) 两个操作数为true,结果才是true,否则是false
逻辑或 |(或) 两个操作数有一个是true,结果就是true
短路与 &&( 与) 只要有一个为false,则直接返回false
短路或 ||(或) 只要有一个为true, 则直接返回true
逻辑非 !(非) 取反:!false为true,!true为false
逻辑异或 ^(异或) 相同为false,不同为true
短路与和短路或采用短路的方式。从左到右计算,如果只通过运算符左边的操作数就能够确定该逻辑表达式的值,则不会继续计算运算符右边的操作数,提高效率。
【示例2-22】短路与和逻辑与
1
2
3
4
5
6 //1>2的结果为false,那么整个表达式的结果即为false,将不再计算2>(3/0)
boolean c = 1>2 && 2>(3/0);
System.out.println©;
//1>2的结果为false,那么整个表达式的结果即为false,还要计算2>(3/0),0不能做除数,//会输出异常信息
boolean d = 1>2 & 2>(3/0);
System.out.println(d);
2.7.5 位运算符
位运算指的是进行二进制位的运算,常用的位运算符如表2-11所示。
表2-11 位运算符
位运算符 说明
~ 取反
& 按位与
| 按位或
^ 按位异或
<< 左移运算符,左移1位相当于乘2
右移运算符,右移1位相当于除2取商
【示例2-23】左移运算和右移运算
1
2
3
4 int a = 322;
int b = 3<<2; //相当于:322;
int c = 12/2/2;
int d = 12>>2; //相当于12/2/2;
雷区
1. &和|既是逻辑运算符,也是位运算符。如果两侧操作数都是boolean类型,就作为逻辑运算符。如果两侧的操作数是整数类型,就是位运算符。
2. 不要把“^”当做数学运算“乘方”,是“位的异或”操作。
2.7.6 字符串连接符
“+”运算符两侧的操作数中只要有一个是字符串(String)类型,系统会自动将另一个操作数转换为字符串然后再进行连接。
【示例2-24】连接符“+”
1
2 int a=12;
System.out.println(“a=”+a);//输出结果: a=12
2.7.7 条件运算符
语法格式:
1 x ? y : z
其中 x 为 boolean 类型表达式,先计算 x 的值,若为true,则整个运算的结果为表达式 y 的值,否则整个运算结果为表达式 z 的值。
【示例2-25】三目条件运算符
1
2
3
4
5
6 int score = 80;
int x = -100;
String type =score<60?“不及格”:“及格”;
int flag = x > 0 ? 1 : (x == 0 ? 0 : -1);
System.out.println("type= " + type);
System.out.println("flag= "+ flag);
运行结果如图2-5所示。
图2-5 示例2-25运行效果图
2.7.8 运算符优先级的问题
表2-12 运算符的优先级
优先级 运算符 类 结合性
1 () 括号运算符 由左至右
2 !、+(正号)、-(负号) 一元运算符 由左至右
2 ~ 位逻辑运算符 由右至左
2 ++、-- 递增与递减运算符 由右至左
3 、/、% 算术运算符 由左至右
4 +、- 算术运算符 由左至右
5 <<、>> 位左移、右移运算符 由左至右
6 >、>=、<、<= 关系运算符 由左至右
7 ==、!= 关系运算符 由左至右
8 & 位运算符、逻辑运算符 由左至右
9 ^ 位运算符、逻辑运算符 由左至右
10 | 位运算符、逻辑运算符 由左至右
11 && 逻辑运算符 由左至右
12 || 逻辑运算符 由左至右
13 ? : 条件运算符 由右至左
14 =、+=、-=、=、/=、%= 赋值运算符、扩展运算符 由右至左
老鸟建议
大家不需要去刻意的记这些优先级,表达式里面优先使用小括号来组织!!
逻辑与、逻辑或、逻辑非的优先级一定要熟悉!(逻辑非>逻辑与>逻辑或)。如:
a||b&&c的运算结果是:a||(b&&c),而不是(a||b)&&c
2.8. 自动类型转换
自动类型转换指的是容量小的数据类型可以自动转换为容量大的数据类型。如图2-6所示,黑色的实线表示无数据丢失的自动类型转换,而虚线表示在转换时可能会有精度的损失。
图2-6 自动类型转换
可以将整型常量直接赋值给byte、 short、 char等类型变量,而不需要进行强制类型转换,只要不超出其表数范围即可。
【示例2-26】自动类型转换特例
1
2 short b = 12; //合法
short b = 1234567;//非法,1234567超出了short的表数范围
2.8.2 强制类型转换
强制类型转换,又被称为造型,用于显式的转换一个数值的类型。在有可能丢失信息的情况下进行的转换是通过造型来完成的,但可能造成精度降低或溢出。
语法格式:
1 (type)var
运算符“()”中的type表示将值var想要转换成的目标数据类型。
2.8.3 基本类型转化时常见错误和问题
操作比较大的数时,要留意是否溢出,尤其是整数操作时。
【示例2-29】常见问题一
1
2
3
4
5
6
7
8
9
10
11 int money = 1000000000; //10亿
int years = 20;
//返回的total是负数,超过了int的范围
int total = moneyyears;
System.out.println(“total=”+total);
//返回的total仍然是负数。默认是int,因此结果会转成int值,再转成long。但是已经发生//了数据丢失
long total1 = moneyyears;
System.out.println(“total1=”+total1);
//返回的total2正确:先将一个因子变成long,整个表达式发生提升。全部用long来计算。
long total2 = money*((long)years);
System.out.println(“total2=”+total2);
运行结果如图2-8所示。
图2-8 示例2-29运行效果图
L和l 的问题:
不要命名名字为l的变量,l容易和1混淆。long类型使用大写L不要用小写。
【示例2-30】常见问题二
1
2
3 int l = 2; //分不清是L还是1,
long a = 23451l;//建议使用大写L
System.out.println(l+1);
【示例2-27】强制类型转换
1
2
3
4
5
6
7 double x = 3.14;
int nx = (int)x; //值为3
char c = ‘a’;
int d = c+1;
System.out.println(nx);
System.out.println(d);
System.out.println((char)d);
运行结果如图2-7所示。
图2-7 示例2-27运行效果图
当将一种类型强制转换成另一种类型,而又超出了目标类型的表数范围,就会被截断成为一个完全不同的值。
【示例2-28】强制类型转换特例
1
2 int x = 300;
byte bx = (byte)x; //值为44
新手雷区
不能在布尔类型和任何数值类型之间做强制类型转换
2.9 简单的键盘输入和输出
为了我们能写出更加复杂的程序,可以让我们的程序和用户可以通过键盘交互,我们先学习一下简单的键盘输入和输出。
【示例2-31】使用Scanner获取键盘输入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 import java.util.Scanner;
/**
*/
public class TestScanner {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println(“请输入名字:”);
String name = scanner.nextLine();
System.out.println(“请输入你的爱好:”);
String favor = scanner.nextLine();
System.out.println(“请输入你的年龄:”);
int age = scanner.nextInt();
System.out.println("###############");
System.out.println(name);
System.out.println(favor);
System.out.println("来到地球的天数:"+age*365);
System.out.println("离开地球的天数:"+(72-age)*365);
}
}
运行结果如图2-9所示。
第二章 总结
1.注释可以提高程序的可读性。可划分为
2.单行注释 //
3.多行注释 /…/
4.文档注释 /**…/
5.标识符的命名规则:
6.标识符必须以字母、下划线_、美元符号
开头。
7.
标识符其它部分可以是字母、下划线
“
”
、美元符“
开头。 7.标识符其它部分可以是字母、下划线“_”、美元符“
开头。7.标识符其它部分可以是字母、下划线“”、美元符“”和数字的任意组合。
8.Java 标识符大小写敏感,且长度无限制。
9.标识符不可以是Java的关键字。
10.标识符的命名规范
11.表示类名的标识符:每个单词的首字母大写,如Man, GoodMan
12.表示方法和变量的标识符:第一个单词小写,从第二个单词开始首字母大写,我们称之为“驼峰原则”,如eat(), eatFood()
13.变量的声明格式:
type varName [=value] [,varName[=value]…];
14.变量的分类:局部变量、实例变量、静态变量
15.常量的声名格式
final type varName = value ;
16.Java的数据类型可分为基本数据类型和引用数据类,基本数据类型的分类如下:
17.整型变量:byte、short、int、long
18.浮点型:float、double
19.字符型:char
20.布尔型:boolean,值为true或者false
21.Java语言支持的运算符可分为:
22.算术运算符: +,-,,/,%,++,–
23.赋值运算符 =
24.扩展赋值运算符:+=,-=,*=,/=
25.关系运算符: >,<,>=,<=,==,!= ,instanceof
26.逻辑运算符: &&,||,!
27.位运算符: &,|,^,~ , >>,<<,>>>
28.字符串连接符:+
29.条件运算符 ?:
30.基本数据类型的类型转换可分为:
31.自动类型转换:容量小的数据类型可以自动转换为容量大的数据类型
32.强制类型转换:用于显式的转换一个数值的类型,语法格式:(type)var
33.键盘的输入:Scanner类的使用
第二章 作业
一、选择题
1.以下选项中属于合法的Java标识符的是( )。(选择二项) c d
A.public
B.3num
C.name
D._age
2.下面的代码段中,执行之后i和j的值是( )。(选择一项)c
int i=1; int j;
j=i++;
A1,1
B.1,2
C.2,1
D.2,2
3.下面的赋值语句中错误的是( )。(选择一项) a
Afloat f = 11.1;
B.double d = 5.3E12;
C.double d = 3.14159;
D.double d = 3.14D;
4.在Java中,下面( )语句能正确通过编译。(选择二项) ac
ASystem.out.println(1+1);
B.char i =2+‘2’;
System.out.println(i);
C.String s=“on”+‘one’;
D.int b=255.0;
5.以下Java运算符中优先级别最低的两个选项是( )。(选择二项) ab
A赋值运算符=
B.条件运算符 ?=
C.逻辑运算符|
D.算术运算符+
二、简答题
1.Java是一种强类型语言,说明Java的数据类型分类。
Java是一种强类型语言,每个变量都必须声明其数据类型。 Java的数据类型可分为两大类:基本数据类型(primitive data type)和引用数据类型(reference data type)。
2.i++和++i的异同之处
两者都是对i的自增操作,i++是先对i进行输出再进行自增,++i则是先进行对i的自增再进行输出。
3.运算符||和|的异同之处
逻辑或|(或),两个操作数有一个是true,结果就是true。短路或,||(或),只要有一个为true, 则直接返回true。 短路与和短路或采用短路的方式。从左到右计算,如果只通过运算符左边的操作数就能够确定该逻辑表达式的值,则不会继续计算运算符右边的操作数,提高效率。
4.Java中基本数据类型转换的规则
自动类型转换指的是容量小的数据类型可以自动转换为容量大的数据类型。如图,黑色的实线表示无数据丢失的自动类型转换,而虚线表示在转换时可能会有精度的损失。
三、编码题
1.输入圆形半径,求圆形的周长和圆形的面积,并将结果输出。
import java.util.Scanner;
public class Second1 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println(“请输入圆的半径:”);
float r = scanner.nextFloat();
System.out.println(“该圆的半径为:R=”+r);
System.out.println(“该圆的周长为:C=23.14"+"”+r+“=”+3.142r);
System.out.println(“该圆的面积为:S=3.14*”+r+"“+r+”="+3.14r*r);
}
}
2.银行利率表如下表所示,请计算存款10000元,活期1年、活期2年,定期1年,定期2年后的本息合计。
结果如下图所示(结果四舍五入,不保留小数位。使用Math.round(double d)实现)。
//Math.round()函数返回一个数字四舍五入后最接近的整数
public class Second1 {
public static void main(String[] args) {
System.out.println(“本金:10000”);
System.out.println(“活期一年本金总计:”+Math.round(100001.0035));
System.out.println(“定期一年本金总计:”+Math.round(100001.015));
System.out.println(“活期两年本金总计:”+Math.round(100000.00352+10000));
System.out.println(“定期两年本金总计:”+Math.round(100000.0212+10000));
}
}
3.某个公司采用公用电话传递数据,数据是四位的整数,在传递过程中是加密的,加密规则如下:每位数字都加上5,然后用和除以10的余数代替该数字,再将第一位和第四位交换,第二位和第三位交换。结果如图所示。
public class Second1 {
public static void main(String[] args) {
showcode();
}
static void showcode() {
Scanner scanner = new Scanner(System.in);
System.out.print(“请输入一个四位数的整数:”);
int num = scanner.nextInt();
Integer inum = Integer.valueOf(num);
if (encode(inum)==0) {
System.out.println(“该数字不符合要求”);
showcode();
}else {
System.out.println(“加密后的数字为:”+encode(inum));
}
}
static int encode(Integer num) {
if((num<=9999) && (num>=1000) && (num instanceof Integer)) {
return (num/1000+5)%10+(((num/100)%10+5)%10)*10+(((num/10)%10+5)%10)*100+(((num%10)+5)%10)*1000;
}
return 0;
}
}
第三章 控制语句
第三章 控制语句
控制语句分为三类:顺序、选择和循环。
“顺序结构”代表“先执行a,再执行b”的逻辑。比如,先找个女朋友,再给女朋友打电话;先订婚,再结婚;
“选择结构”代表“如果…,则…”的逻辑。比如,如果女朋友来电,则迅速接电话;如果看到红灯,则停车;
“循环结构”代表“如果…,则再继续…”的逻辑。比如,如果没打通女朋友电话,则再继续打一次; 如果没找到喜欢的人,则再继续找。
3.1 选择结构
在还没有知道Java选择结构的时候,我们编写的程序总是从程序入口开始,顺序执行每一条语句直到执行完最后一条语句结束,但是生活中经常需要进行条件判断,根据判断结果决定是否做一件事情,这就需要选择结构。
选择结构用于判断给定的条件,然后根据判断的结果来控制程序的流程。
主要的选择结构有:if选择结构和switch多选择结构。有如下结构:
if单选择结构
if-else双选择结构
if-else if-else多选择结构
switch结构
3.1.1 if单选择结构
语法结构:
1
2
3 if(布尔表达式){
语句块
}
if语句对布尔表达式进行一次判定,若判定为真,则执行{}中的语句块,否则跳过该语句块。流程图如图3-1所示。
图3-1 if单选择结构流程图
【示例3-1】if单选择结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 public class Test1 {
public static void main(String[] args) {
//通过掷三个骰子看看今天的手气如何?
int i = (int)(6 * Math.random()) + 1;//通过Math.random()产生随机数
int j = (int)(6 * Math.random()) + 1;
int k = (int)(6 * Math.random()) + 1;
int count = i + j + k;
//如果三个骰子之和大于15,则手气不错
if(count > 15) {
System.out.println(“今天手气不错”);
}
//如果三个骰子之和在10到15之间,则手气一般
if(count >= 10 && count <= 15) { //错误写法:10<=count<=15
System.out.println(“今天手气很一般”);
}
//如果三个骰子之和小于10,则手气不怎么样
if(count < 10) {
System.out.println(“今天手气不怎么样”);
}
System.out.println(“得了” + count + “分”);
}
}
图3-2 示例3-1运行效果图
Math类的使用
1.java.lang包中的Math类提供了一些用于数学计算的方法。
2.Math.random()该方法用于产生一个0到1区间的double类型的随机数,但是不包括1。[0,1)ss
int i = (int) (6 * Math.random()); //产生:[0,5]之间的随机整数
新手雷区
1.如果if语句不写{},则只能作用于后面的第一条语句。
2.强烈建议,任何时候都写上{},即使里面只有一句话
3.1.2 if-else双选择结构
语法结构:
1
2
3
4
5 if(布尔表达式){
语句块1
}else{
语句块2
}
当布尔表达式为真时,执行语句块1,否则,执行语句块2。也就是else部分。流程图如图3-3所示。
图3-3 if-else双选择结构流程图
【示例3-2】if-else结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 public class Test2 {
public static void main(String[] args) {
//随机产生一个[0.0, 4.0)区间的半径,并根据半径求圆的面积和周长
double r = 4 * Math.random();
//Math.pow(r, 2)求半径r的平方
double area = Math.PI * Math.pow(r, 2);
double circle = 2 * Math.PI * r;
System.out.println("半径为: " + r);
System.out.println("面积为: " + area);
System.out.println(“周长为: " + circle);
//如果面积>=周长,则输出"面积大于等于周长”,否则,输出周长大于面积
if(area >= circle) {
System.out.println(“面积大于等于周长”);
} else {
System.out.println(“周长大于面积”);
}
}
}
图3-4 示例3-2运行效果图
条件运算符有时候可用于代替if-else,如示例3-3与示例3-4所示。
【示例3-3】使用if-else
1
2
3
4
5
6
7
8
9
10
11 public class Test3 {
public static void main(String[] args) {
int a=2;
int b=3;
if (a<b) {
System.out.println(a);
} else {
System.out.println(b);
}
}
}
图3-5 示例3-3运行效果图
【示例3-4】使用条件运算符
1
2
3
4
5
6
7 public class Test4 {
public static void main(String[] args) {
int a=2;
int b=3;
System.out.println((a<b)?a:b);
}
}
图3-6 示例3-4运行效果图
3.1.3 if-else if-else多选择结构
语法结构:
1
2
3
4
5
6
7
8
9
10 if(布尔表达式1) {
语句块1;
} else if(布尔表达式2) {
语句块2;
}……
else if(布尔表达式n){
语句块n;
} else {
语句块n+1;
}
当布尔表达式1为真时,执行语句块1;否则,判断布尔表达式2,当布尔表达式2为真时,执行语句块2;否则,继续判断布尔表达式3······;如果1~n个布尔表达式均判定为假时,则执行语句块n+1,也就是else部分。流程图如图3-7所示。
图3-7 if-else if-else多选择结构流程图
【示例3-5】if-else if-else多选择结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 public class Test5 {
public static void main(String[] args) {
int age = (int) (100 * Math.random());
System.out.print(“年龄是” + age + “, 属于”);
if (age < 15) {
System.out.println(“儿童, 喜欢玩!”);
} else if (age < 25) {
System.out.println(“青年, 要学习!”);
} else if (age < 45) {
System.out.println(“中年, 要工作!”);
} else if (age < 65) {
System.out.println(“中老年, 要补钙!”);
} else if (age < 85) {
System.out.println(“老年, 多运动!”);
} else {
System.out.println(“老寿星, 古来稀!”);
}
}
}
图3-8 示例3-5运行效果图1
图3-9 示例3-5运行效果图2
课堂练习
仿照【示例3-5】,实现如下功能:
随机生成一个100以内的成绩,当成绩在85及以上的时候输出”等级A”,70以上到84之间输出”等级B”,60到69之间输出”等级C”,60以下输出”等级D”。
3.1.4 switch多选择结构
语法结构:
1
2
3
4
5
6
7
8
9
10
11 switch (表达式) {
case 值1:
语句序列1;
[break];
case 值2:
语句序列2;
[break];
… … … … …
[default:
默认语句;]
}
switch语句会根据表达式的值从相匹配的case标签处开始执行,一直执行到break语句处或者是switch语句的末尾。如果表达式的值与任一case值不匹配,则进入default语句(如果存在default语句的情况)。
根据表达式值的不同可以执行许多不同的操作。switch语句中case标签在JDK1.5之前必须是整数(long类型除外)或者枚举,不能是字符串,在JDK1.7之后允许使用字符串(String)。
大家要注意,当布尔表达式是等值判断的情况,可以使用if-else if-else多选择结构或者switch结构,如果布尔表达式区间判断的情况,则只能使用if-else if-else多选择结构。
switch多选择结构的流程图如图3-10所示。
图3-10 switch多选择结构流程图
【示例3-6】switch结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 public class Test6 {
public static void main(String[] args) {
char c = ‘a’;
int rand = (int) (26 * Math.random());
char c2 = (char) (c + rand);
System.out.print(c2 + ": ");
switch (c2) {
case ‘a’:
case ‘e’:
case ‘i’:
case ‘o’:
case ‘u’:
System.out.println(“元音”);
break;
case ‘y’:
case ‘w’:
System.out.println(“半元音”);
break;
default:
System.out.println(“辅音”);
}
}
}
图3-11 示例3-6运行效果图1
图3-12 示例3-6运行效果图2
3.2 循环结构
循环结构分两大类,一类是当型,一类是直到型。
当型:
当布尔表达式条件为true时,反复执行某语句,当布尔表达式的值为false时才停止循环,比如:while与for循环。
直到型:
先执行某语句, 再判断布尔表达式,如果为true,再执行某语句,如此反复,直到布尔表达式条件为false时才停止循环,比如do-while循环。
3.2.1 while循环
语法结构:
1
2
3 while (布尔表达式) {
循环体;
}
在循环刚开始时,会计算一次“布尔表达式”的值,若条件为真,执行循环体。而对于后来每一次额外的循环,都会在开始前重新计算一次。
语句中应有使循环趋向于结束的语句,否则会出现无限循环–––"死"循环。
while循环结构流程图如图3.13所示。
图3-13 while流程图
【示例3-7】while循环结构:求1到100之间的累加和
1
2
3
4
5
6
7
8
9
10
11
12 public class Test7 {
public static void main(String[] args) {
int i = 0;
int sum = 0;
// 1+2+3+…+100=?
while (i <= 100) {
sum += i;//相当于sum = sum+i;
i++;
}
System.out.println("Sum= " + sum);
}
}
图3-14 示例3-7运行效果图
3.2.2 do-while循环
语法结构:
1
2
3 do {
循环体;
} while(布尔表达式) ;
do-while循环结构会先执行循环体,然后再判断布尔表达式的值,若条件为真,执行循环体,当条件为假时结束循环。do-while循环的循环体至少执行一次。do-while循环结构流程图如图3.15所示。
图3-15 do-while流程图
【示例3-8】do-while循环结构:1-100之间的累加和
1
2
3
4
5
6
7
8
9
10
11 public class Test8 {
public static void main(String[] args) {
int i = 0;
int sum = 0;
do {
sum += i; // sum = sum + i
i++;
} while (i <= 100);//此处的;不能省略
System.out.println("Sum= " + sum);
}
}
图3-16 示例3-8运行效果图
【示例3-9】while与do-while的区别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 public class Test9 {
public static void main(String[] args) {
//while循环:先判断再执行
int a = 0;
while (a < 0) {
System.out.println(a);
a++;
}
System.out.println(“-----”);
//do-while循环:先执行再判断
a = 0;
do {
System.out.println(a);
a++;
} while (a < 0);
}
}
图3-17 示例3-9运行效果图
从运行效图中可以看出do-while总是保证循环体至少会被执行一次!
3.2.3 for循环
语法结构:
1
2
3 for (初始表达式; 布尔表达式; 迭代因子) {
循环体;
}
for循环语句是支持迭代的一种通用结构,是最有效、最灵活的循环结构。for循环在第一次反复之前要进行初始化,即执行初始表达式;随后,对布尔表达式进行判定,若判定结果为true,则执行循环体,否则,终止循环;最后在每一次反复的时候,进行某种形式的“步进”,即执行迭代因子。
A. 初始化部分设置循环变量的初值
B. 条件判断部分为任意布尔表达式
C. 迭代因子控制循环变量的增减
for循环在执行条件判定后,先执行的循环体部分,再执行步进。
for循环结构的流程图如图3-18所示。
图3-18 for循环流程图
【示例3-10】for循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 public class Test10 {
public static void main(String args[]) {
int sum = 0;
//1.求1-100之间的累加和
for (int i = 0; i <= 100; i++) {
sum += i;
}
System.out.println(“Sum= " + sum);
//2.循环输出9-1之间的数
for(int i=9;i>0;i–){
System.out.print(i+”、“);
}
System.out.println();
//3.输出90-1之间能被3整除的数
for(int i=90;i>0;i-=3){
System.out.print(i+”、");
}
System.out.println();
}
}
图3-19 示例3-10运行效果图
Java里能用到逗号运算符的地方屈指可数,其中一处就是for循环的控制表达式。在控制表达式的初始化和步进控制部分,我们可以使用一系列由逗号分隔的表达式,而且那些表达式均会独立执行。
【示例3-11】逗号运算符
1
2
3
4
5
6
7 public class Test11 {
public static void main(String[] args) {
for(int i = 1, j = i + 10; i < 5; i++, j = i * 2) {
System.out.println("i= " + i + " j= " + j);
}
}
}
图3-20 示例3-11运行效果图
1. 无论在初始化还是在步进部分,语句都是顺序执行的。
2. 尽管初始化部分可设置任意数量的定义,但都属于同一类型。
3. 约定:只在for语句的控制表达式中写入与循环变量初始化,条件判断和迭代因子相关的表达式。
初始化部分、条件判断部分和迭代因子可以为空语句,但必须以“;”分开,如示例3-12所示。
【示例3-12】无限循环
1
2
3
4
5
6
7 public class Test12 {
public static void main(String[] args) {
for ( ; ; ) { // 无限循环: 相当于 while(true)
System.out.println(“北京尚学堂”);
}
}
}
编译器将while(true)与for(;;)看作同一回事,都指的是无限循环。
在for语句的初始化部分声明的变量,其作用域为整个for循环体,不能在循环外部使用该变量。如示例3-13所示。
【示例3-13】初始化变量的作用域
3.2.4 嵌套循环
在一个循环语句内部再嵌套一个或多个循环,称为嵌套循环。while、do-while与for循环可以任意嵌套多层。
【示例3-14】嵌套循环
1
2
3
4
5
6
7
8
9
10 public class Test14 {
public static void main(String args[]) {
for (int i=1; i <=5; i++) {
for(int j=1; j<=5; j++){
System.out.print(i+" ");
}
System.out.println();
}
}
}
图3-21 示例3-14运行效果图
【示例3-15】使用嵌套循环实现九九乘法表
1
2
3
4
5
6
7
8
9
10 public class Test15 {
public static void main(String args[]) {
for (int i = 1; i < 10; i++) { // i是一个乘数
for (int j = 1; j <= i; j++) { // j是另一个乘数
System.out.print(j + “*” + i + “=” + (i * j < 10 ? (" " + i * j) : i * j) + " ");
}
System.out.println();
}
}
}
图3-22 示例3-15运行效果图
课堂练习
1. 用while循环分别计算100以内的奇数及偶数的和,并输出。
2. 用while循环或其他循环输出1-1000之间能被5整除的数,且每行输出5个。
3.2.5 break语句和continue语句
在任何循环语句的主体部分,均可用break控制循环的流程。break用于强行退出循环,不执行循环中剩余的语句。
【示例3-16】break语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 public class Test16 {
public static void main(String[] args) {
int total = 0;//定义计数器
System.out.println(“Begin”);
while (true) {
total++;//每循环一次计数器加1
int i = (int) Math.round(100 * Math.random());
//当i等于88时,退出循环
if (i == 88) {
break;
}
}
//输出循环的次数
System.out.println(“Game over, used " + total + " times.”);
}
}
图3-23 示例3-16运行效果图
continue 语句用在循环语句体中,用于终止某次循环过程,即跳过循环体中尚未执行的语句,接着进行下一次是否执行循环的判定。
注意事项
1. continue用在while,do-while中,continue 语句立刻跳到循环首部,越过了当前循环的其余部分。
2. continue用在for循环中,跳到for循环的迭代因子部分。
【示例3-17】continue语句:把100~150之间不能被3整除的数输出,并且每行输出5个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 public class Test17 {
public static void main(String[] args) {
int count = 0;//定义计数器
for (int i = 100; i < 150; i++) {
//如果是3的倍数,则跳过本次循环,继续进行下一次循环
if (i % 3 == 0){
continue;
}
//否则(不是3的倍数),输出该数
System.out.print(i + “、”);
count++;//没输出一个数,计数器加1
//根据计数器判断每行是否已经输出了5个数
if (count % 5 == 0) {
System.out.println();
}
}
}
}
图3-24 示例3-17运行效果图
3.2.6 带标签的break和continue
goto关键字很早就在程序设计语言中出现。尽管goto仍是Java的一个保留字,但并未在Java语言中得到正式使用;Java没有goto语句。然而,在break和continue这两个关键字的身上,我们仍然能看出一些goto的影子---带标签的break和continue。
“标签”是指后面跟一个冒号的标识符,例如:“label:”。对Java来说唯一用到标签的地方是在循环语句之前。而在循环之前设置标签的唯一理由是:我们希望在其中嵌套另一个循环,由于break和continue关键字通常只中断当前循环,但若随同标签使用,它们就会中断到存在标签的地方。
在 “goto有害”论中,最有问题的就是标签,而非goto, 随着标签在一个程序里数量的增多,产生错误的机会也越来越多。 但Java标签不会造成这方面的问题,因为它们的活动场所已被限死,不可通过特别的方式到处传递程序的控制权。由此也引出了一个有趣的问题:通过限制语句的能力,反而能使一项语言特性更加有用。
【示例3-18】带标签break和continue:控制嵌套循环跳转(打印101-150之间所有的质数)
1
2
3
4
5
6
7
8
9
10
11
12 public class Test18 {
public static void main(String args[]) {
outer: for (int i = 101; i < 150; i++) {
for (int j = 2; j < i / 2; j++) {
if (i % j == 0){
continue outer;
}
}
System.out.print(i + " ");
}
}
}
图3-25 示例3-18运行效果图
3.3 语句块
语句块(有时叫做复合语句),是用花括号扩起的任意数量的简单Java语句。块确定了局部变量的作用域。块中的程序代码,作为一个整体,是要被一起执行的。块可以被嵌套在另一个块中,但是不能在两个嵌套的块内声明同名的变量。语句块可以使用外部的变量,而外部不能使用语句块中定义的变量,因为语句块中定义的变量作用域只限于语句块。
【示例3-19】语句块
1
2
3
4
5
6
7
8
9
10 public class Test19 {
public static void main(String[] args) {
int n;
int a;
{
int k;
int n; //编译错误:不能重复定义变量n
} //变量k的作用域到此为止
}
}
3.4 方法
方法就是一段用来完成特定功能的代码片段,类似于其它语言的函数。
方法用于定义该类或该类的实例的行为特征和功能实现。 方法是类和对象行为特征的抽象。方法很类似于面向过程中的函数。面向过程中,函数是最基本单位,整个程序由一个个函数调用组成。面向对象中,整个程序的基本单位是类,方法是从属于类和对象的。
方法声明格式:
1
2
3 [修饰符1 修饰符2 …] 返回值类型 方法名(形式参数列表){
Java语句;… … …
}
方法的调用方式:
对象名.方法名(实参列表)
方法的详细说明
1. 形式参数:在方法声明时用于接收外界传入的数据。
2. 实参:调用方法时实际传给方法的数据。
3. 返回值:方法在执行完毕后返还给调用它的环境的数据。
4. 返回值类型:事先约定的返回值的数据类型,如无返回值,必须显示指定为为void。
【示例3-20】方法的声明及调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 public class Test20 {
/** main方法:程序的入口 /
public static void main(String[] args) {
int num1 = 10;
int num2 = 20;
//调用求和的方法:将num1与num2的值传给add方法中的n1与n2
// 求完和后将结果返回,用sum接收结果
int sum = add(num1, num2);
System.out.println("sum = " + sum);//输出:sum = 30
//调用打印的方法:该方法没有返回值
print();
}
/* 求和的方法 /
public static int add(int n1, int n2) {
int sum = n1 + n2;
return sum;//使用return返回计算的结果
}
/* 打印的方法 */
public static void print() {
System.out.println(“北京尚学堂…”);
}
}
图3-26 示例3-20运行效果图
注意事项
1. 实参的数目、数据类型和次序必须和所调用的方法声明的形式参数列表匹配。
2. return 语句终止方法的运行并指定要返回的数据。
3. Java中进行方法调用中传递参数时,遵循值传递的原则(传递的都是数据的副本):
4. 基本类型传递的是该数据值的copy值。
5. 引用类型传递的是该对象引用的copy值,但指向的是同一个对象。
3.5 方法的重载(overload)
方法的重载是指一个类中可以定义多个方法名相同,但参数不同的方法。 调用时,会根据不同的参数自动匹配对应的方法。
雷区
重载的方法,实际是完全不同的方法,只是名称相同而已!
构成方法重载的条件:
1.不同的含义:形参类型、形参个数、形参顺序不同
2.只有返回值不同不构成方法的重载
如:
1 int a(String str){}与 void a(String str){}
不构成方法重载
3.只有形参的名称不同,不构成方法的重载
如:
1 int a(String str){}与int a(String s){}
不构成方法重载
【示例3-21】方法重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42 public class Test21 {
public static void main(String[] args) {
System.out.println(add(3, 5));// 8
System.out.println(add(3, 5, 10));// 18
System.out.println(add(3.0, 5));// 8.0
System.out.println(add(3, 5.0));// 8.0
// 我们已经见过的方法的重载
System.out.println();// 0个参数
System.out.println(1);// 参数是1个int
System.out.println(3.0);// 参数是1个double
}
/** 求和的方法 /
public static int add(int n1, int n2) {
int sum = n1 + n2;
return sum;
}
// 方法名相同,参数个数不同,构成重载
public static int add(int n1, int n2, int n3) {
int sum = n1 + n2 + n3;
return sum;
}
// 方法名相同,参数类型不同,构成重载
public static double add(double n1, int n2) {
double sum = n1 + n2;
return sum;
}
// 方法名相同,参数顺序不同,构成重载
public static double add(int n1, double n2) {
double sum = n1 + n2;
return sum;
}
//编译错误:只有返回值不同,不构成方法的重载
public static double add(int n1, int n2) {
double sum = n1 + n2;
return sum;
}
//编译错误:只有参数名称不同,不构成方法的重载
public static int add(int n2, int n1) {
double sum = n1 + n2;
return sum;
}
}
3.6 递归结构
递归是一种常见的解决问题的方法,即把问题逐渐简单化。递归的基本思想就是“自己调用自己”,一个使用递归技术的方法将会直接或者间接的调用自己。
利用递归可以用简单的程序来解决一些复杂的问题。比如:斐波那契数列的计算、汉诺塔、快排等问题。
递归结构包括两个部分:
1.定义递归头。解答:什么时候不调用自身方法。如果没有头,将陷入死循环,也就是递归的结束条件。
2.递归体。解答:什么时候需要调用自身方法。
【示例3-22】递归:计算n!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 public class Test22 {
public static void main(String[] args) {
long d1 = System.currentTimeMillis();
System.out.printf(“%d阶乘的结果:%s%n”, 10, factorial(10));
long d2 = System.currentTimeMillis();
System.out.printf(“递归费时:%s%n”, d2-d1); //耗时:32ms
}
/* 求阶乘的方法*/
static long factorial(int n){
if(n==1){//递归头
return 1;
}else{//递归体
return n*factorial(n-1);//n! = n * (n-1)!
}
}
}
图3-27 示例3-22运行效果图
图3-28 递归原理分析图
递归的缺陷
简单的程序是递归的优点之一。但是递归调用会占用大量的系统堆栈,内存耗用多,在递归调用层次多时速度要比循环慢的多,所以在使用递归时要慎重。
比如上面的递归耗时558ms。但是用普通循环的话快得多,如示例3-23所示。
【示例3-23】使用循环求n!
1
2
3
4
5
6
7
8
9
10
11
12
13
14 public class Test23 {
public static void main(String[] args) {
long d3 = System.currentTimeMillis();
int a = 10;
int result = 1;
while (a > 1) {
result *= a * (a - 1);
a -= 2;
}
long d4 = System.currentTimeMillis();
System.out.println(result);
System.out.printf(“普通循环费时:%s%n”, d4 - d3);
}
}
图3-29 示例3-23运行效果图
注意事项
任何能用递归解决的问题也能使用迭代解决。当递归方法可以更加自然地反映问题,并且易于理解和调试,并且不强调效率问题时,可以采用递归;
在要求高性能的情况下尽量避免使用递归,递归调用既花时间又耗内存。
第三章 总结
1.从结构化程序设计角度出发,程序有三种结构:顺序结构、选择结构和循环结构
2.选择结构
(1)if单选择结构 if-else双选择结构 if-else if-else多选择结构
(2)switch多选择结构
3.多选择结构与switch的关系:当布尔表达式是等值判断的情况,可使用多重选择结构或switch结构,如果布尔表达式区间判断的情况,则只能使用多重选择结构
(1) 循环结构
(2)当型:while与for
(3)直到型:do-while
4.while与do-while的区别,在布尔表达式的值为false时while的循环体一次也不执行,而do-while至少执行一次
5.break可以在switch与循环结构中使用,而continue只能在循环结构中使用
6.方法就是一段用来完成特定功能的代码片段,类似于其它语言的函数
7.方法的重载是指一个类中可以定义多个方法名相同,但参数不同的方法。 调用时,会根据不同的参数自动匹配对应的方法
8.任何能用递归解决的问题也能使用迭代解决。在要求高性能的情况下尽量避免使用递归,递归调用既花时间又耗内存。
第三章 作业
一、选择题
1.分析如下Java代码,编译运行的输出结果是( )。(选择一项) a
1
2
3
4
5
6
7
8
9
10
11 public static void main(String[ ] args) {
boolean a=true;
boolean b=false;
if (!(a&&b)) {
System.out.print(“!(a&&b)”);
}else if (!(a||b)) {
System.out.println(“!(a||b)”);
}else {
System.out.println(“ab”);
}
}
A!(a&&b)
B.!(a||b)
C.ab
D.!(a||b)ab
2.下列选项中关于变量x的定义,( )可使以下switch语句编译通过。(选择二项) bd
1
2
3
4
5
6
7
8
9
10
11
12
13 switch(x) {
case 100 :
System.out.println(“One hundred”);
break;
case 200 :
System.out.println(“Two hundred”);
break;
case 300 :
System.out.println( “Three hundred”);
break;
default :
System.out.println( “default”);
}
Adouble x = 100;
B.char x = 100;
C.String x = “100”;
D.int x = 100;
3.给定如下Java代码,编译运行的结果是( )。(选择一项) a
1
2
3
4
5
6
7
8
9
10
11
12
13 public class Test {
public static void main(String[] args) {
int sum=0;
for(int i=1;i<10;i++){
do{
i++;//i=2 sum=0 i=3 sum=3 i=4 i=5 sum=8 i=6
if(i%2!=0)
sum+=i;
}while(i<6);
}
System.out.println(sum);
}
}
A8
B.15
C.24
D.什么也不输出
4.以下选项中添加到代码中横线处会出现错误的是( )。(选择二项) bd
1
2
3
4
5 public class Test {
public float aMethod(float a, float b) {
return 0;
}
}
A.
1
2
3 public float aMethod(float a, float b, float c) {
return 0;
}
B.
1
2
3 public float aMethod(float c, float d) {
return 0;
}
C.
1
2
3 public int aMethod(int a, int b) {
return 0;
}
D.
1
2
3 private int aMethod(float a, float b) {
return 0;
}
5.以下关于方法调用的代码的执行结果是( )。(选择一项) b
1
2
3
4
5
6
7
8
9
10 public class Test {
public static void main(String args[]) {
int i = 99;
mb_operate(i);
System.out.print(i + 100);
}
static void mb_operate(int i) {
i += 100;
}
}
A.99
B.199
C.299
D.99100
二、简答题
1. if多分支语句和switch语句的异同之处。
异:多选择结构与switch的关系:当布尔表达式是等值判断的情况,可使用多重选择结构或switch结构,
如果布尔表达式区间判断的情况,则只能使用多重选择结构。
同:都是选择结构
2. break和continue语句的作用。
break退出当前循环,continue则是退出本次循环,直接跳到循环末尾进行下一次循环。
3. 在多重循环中,如何在内层循环中使用break跳出外层循环。
使用标签break退出外层循环。
例:
outLoop: for(int i=1;i<=5;i++) { //break标签
for(int j=1;j<=5;j++) {
if(i==3)
break outLoop; //使用break 标签
System.out.println("i:"+i+" j:"+j);
}
}
}
4. 方法重载的定义、作用和判断依据。
方法的重载是指一个类中可以定义多个方法名相同,但参数不同的方法。 调用时,会根据不同的参数自动匹配对应的方法。
作用:用相同方法名,不同的含义(形参类型、形参个数、形参顺序)来表达不同的方法
判断依据:看形参类型、形参个数、形参顺序是否相同
三、编码题
从键盘输入某个十进制整数数,转换成对应的二进制整数并输出。
import java.util.ArrayList;
import java.util.Scanner;
public class Third1 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println(“请输入一个整数:”);
int num = scanner.nextInt();
ArrayList nums = new ArrayList<>();
java.util.Collections.reverse(nums);
code(num,nums);
System.out.print(“该整数的二进制数为:”);
for (Integer integer : nums) {
System.out.print(integer);
}
}
static void code(int num,ArrayList nums) {
if(num>0) {
nums.add(num%2);
num /= 2;
code(num,nums);
}
}
}
确实,使用Collections.reverse结合一定方法可以实现对list集合降序排序,但是直接使用Collections.reverse(list)这种方式来降序是错误的。
reverse的意思是反转,而不是降序。只是将list集合原来的顺序反转了一下,反转并不意味着降序了。所以要想实现降序,可以先对集合进行升序,然后再反转,这样就降序了。
举个例子:
import java.util.*;
public class Test {
private static Map<Integer, String> map = new HashMap<Integer, String>();
public static void main(String[] args) {
long[] data = {1506326821000l, 1506327060000l, 1506326880000l, 1506327000000l, 1506326940000l, 1506326760000l, 1506326700000l};
List list = new ArrayList<>();
for (long key : data) {
list.add(key);
}
System.out.println(list);
//先升序
Collections.sort(list);
System.out.println(list);
//再反转
Collections.reverse(list);
System.out.println(list);
}
}
2. 编程求:∑1+∑2+……+∑100。
public class Third2 {
public static void main(String[] args) {
int sum = 0;
for(int i=1;i<=100;i++) {
sum += add(i);
}
System.out.println(sum);
}
static int add(int num) {
if(num==1) {
return 1;
}else {
return (num+add(--num));
}
}s
}
3. 编写递归算法程序:一列数的规则如下: 1、1、2、3、5、8、13、21、34… 求数列的第40位数是多少。
public class Third3 {
static int i =1;
static int inum1 =1;
static int inum2 =1;
public static void main(String[] args) {
System.out.println(add(40));
}
static int add(int num) {
if(i<num-1){
int k = inum2;
inum2 = inum1+inum2;
inum1 = k;
i++;
add(num);
}
return inum2;
}
}
第四章 Java面向对象基础
4.1 面向过程和面向对象
面向过程(Procedure Oriented)和面向对象(Object Oriented,OO)都是对软件分析、设计和开发的一种思想,它指导着人们以不同的方式去分析、设计和开发软件。早期先有面向过程思想,随着软件规模的扩大,问题复杂性的提高,面向过程的弊端越来越明显的显示出来,出现了面向对象思想并成为目前主流的方式。两者都贯穿于软件分析、设计和开发各个阶段,对应面向对象就分别称为面向对象分析(OOA)、面向对象设计(OOD)和面向对象编程(OOP)。C语言是一种典型的面向过程语言,Java是一种典型的面向对象语言。
面向过程思想思考问题时,我们首先思考“怎么按步骤实现?”并将步骤对应成方法,一步一步,最终完成。 这个适合简单任务,不需要过多协作的情况下。比如,如何开车?我们很容易就列出实现步骤:
1. 发动车 2. 挂挡 3.踩油门 4. 走你
面向过程适合简单、不需要协作的事务。 但是当我们思考比较复杂的问题,比如“如何造车?”,就会发现列出1234这样的步骤,是不可能的。那是因为,造车太复杂,需要很多协作才能完成。此时面向对象思想就应运而生了。
面向对象(Object)思想更契合人的思维模式。我们首先思考的是“怎么设计这个事物?” 比如思考造车,我们就会先思考“车怎么设计?”,而不是“怎么按步骤造车的问题”。这就是思维方式的转变。
一、面向对象思想思考造车,发现车由如下对象组成:
1. 轮胎
2. 发动机
3. 车壳
4. 座椅
5. 挡风玻璃
为了便于协作,我们找轮胎厂完成制造轮胎的步骤,发动机厂完成制造发动机的步骤;这样,发现大家可以同时进行车的制造,最终进行组装,大大提高了效率。但是,具体到轮胎厂的一个流水线操作,仍然是有步骤的,还是离不开面向过程思想!
因此,面向对象可以帮助我们从宏观上把握、从整体上分析整个系统。 但是,具体到实现部分的微观操作(就是一个个方法),仍然需要面向过程的思路去处理。
我们千万不要把面向过程和面向对象对立起来。他们是相辅相成的。面向对象离不开面向过程!
·面向对象和面向过程的总结
1、都是解决问题的思维方式,都是代码组织的方式。
2、解决简单问题可以使用面向过程
3、解决复杂问题:宏观上使用面向对象把握,微观处理上仍然是面向过程。
· 面向对象思考方式
遇到复杂问题,先从问题中找名词,然后确立这些名词哪些可以作为类,再根据问题需求确定的类的属性和方法,确定类之间的关系。
建议
1.面向对象具有三大特征:封装性、继承性和多态性,而面向过程没有继承性和多态性,并且面向过程的封装只是封装功能,而面向对象可以封装数据和功能。所以面向对象优势更明显。
2.一个经典的比喻:面向对象是盖浇饭、面向过程是蛋炒饭。盖浇饭的好处就是“菜”“饭”分离,从而提高了制作盖浇饭的灵活性。饭不满意就换饭,菜不满意换菜。用软件工程的专业术语就是“可维护性”比较好,“饭” 和“菜”的耦合度比较低。
4.2 对象的进化史(数据管理和企业管理共通之处)
事物的发展总是遵循“量变引起质变”的哲学原则;企业管理和数据管理、甚至社会管理也有很多共通的地方。本节课类比企业发展,让大家更容易理解为什么会产生“对象”这个概念。
·数据无管理时代
最初的计算机语言只有基本变量(类似我们学习的基本数据类型),用来保存数据。那时候面对的数据非常简单,只需要几个变量即可搞定;这个时候不涉及“数据管理”的问题。同理,就像在企业最初发展阶段只有几个人,不涉及管理问题,大家闷头做事就OK了。
·数组管理和企业部门制
企业发展中,员工多了怎么办?我们很自然的想法就是归类,将类型一致的人放到一起;企业中,会将都做销售工作的放到销售部管理;会将研发软件的放到开发部管理。同理在编程中,变量多了,我们很容易的想法就是“将同类型数据放到一起”, 于是就形成了“数组”的概念,单词对应“array”。 这种“归类”的思想,便于管理数据、管理人。
·对象和企业项目制
企业继续发展,面对的场景更加复杂。一个项目可能需要经常协同多个部门才能完成工作;一个项目从谈判接触可能需要销售部介入;谈判完成后,需求调研开始,研发部和销售部一起介入;开发阶段需要开发部和测试部互相配合敏捷开发,同时整个过程财务部也需要跟进。在企业中,为了便于协作和管理,很自然就兴起了“项目制”,以项目组的形式组织,一个项目组可能包含各种类型的人员。 一个完整的项目组,麻雀虽小五脏俱全,就是个创业公司甚至小型公司的编制,包含行政后勤人员、财务核算人员、开发人员、售前人员、售后人员、测试人员、设计人员等等。事实上,华为、腾讯、阿里巴巴等大型公司内部都是采用这种“项目制”的方式进行管理。
同理,计算机编程继续发展,各种类型的变量更加多了,而且对数据的操作(指的就是方法,方法可以看做是对数据操作的管理)也复杂了,怎么办?
为了便于协作和管理,我们“将相关数据和相关方法封装到一个独立的实体”,于是“对象”产生了。 比如,我们的一个学生对象:
有属性(静态特征):年龄:18,姓名:高淇,学号:1234
也可以有方法(动态行为):学习,吃饭,考试
请大家举一反三,根据上表理解一下企业的进化史,会发现大道至简。原来,数据管理、企业管理、社会发展也是有很多共通的地方。“量变引起质变,不同的数量级必然采用不同的管理模式”。
表4-1对象进化史和企业进化史
对象进化史 企业进化史 抽象类比
数据少时 基本类型数据阶段 人少时 作坊时代 无管理时代
(对数据或人没有任何管理)
数据多了 数组
同类型数据存到数组中 人多了 部门
工作行为一样的在一个部门 弱管理时代
(将同类型数据集中进行管理)
数据多了
数据关系/操作复杂了 类和对象
将数据和相关的操作行为放到一起 人多了,
业务更复杂,
人的工作行为也复杂 项目组
将不同类型的人放到一起实现统一管理 强管理时代
(将数据和数据操作/人和人的行为 放到一起管理)
总结
1.对象说白了也是一种数据结构(对数据的管理模式),将数据和数据的行为放到了一起。
2.在内存上,对象就是一个内存块,存放了相关的数据集合!
3.对象的本质就一种数据的组织方式!
4.3 对象和类的概念
我们人认识世界,其实就是面向对象的(此对象可不是男女谈对象的彼对象呀)。比如现在让大家认识一下“天使”这个新事物,天使大家没见过吧,怎么样认识呢?最好的办法就是,给你们面前摆4个天使,带翅膀的美女,让大家看,看完以后,即使我不说,大家下一次是不是就都认识天使了。
图4-1 认识天使
但是,看完10个天使后,我们总要总结一下,什么样的东东才算天使?天使是无数的,总有没见过的!所以必须总结抽象,便于认识未知事物!总结的过程就是抽象的过程。小时候,我们学自然数时怎么定义的?像1,2,3,4…这样的数就叫做自然数。 通过抽象,我们发现天使有这样一下特征:
1. 带翅膀(带翅膀不一定是天使,还可能是鸟人)
2. 女孩(天使掉下来脸着地,也是天使!)
3. 善良
4. 头上有光环
那么通过这4个具体的天使,我们进行抽象,抽象出了天使的特征,我们也可以归纳一个天使类。 通过这个过程,类就是对象的抽象。
类可以看做是一个模版,或者图纸,系统根据类的定义来造出对象。我们要造一个汽车,怎么样造?类就是这个图纸,规定了汽车的详细信息,然后根据图纸将汽车造出来。
类:我们叫做class。 对象:我们叫做Object,instance(实例)。以后我们说某个类的对象,某个类的实例。是一样的意思。
总结
1.对象是具体的事物;类是对对象的抽象;
2.类可以看成一类对象的模板,对象可以看成该类的一个具体实例。
3.类是用于描述同一类型的对象的一个抽象概念,类中定义了这一类对象所应具有的共同的属性、方法。
4.3.1 第一个类的定义
【示例4-1】类的定义方式
1
2
3
4
5
6
7
8
9 // 每一个源文件必须有且只有一个public class,并且类名和文件名保持一致!
public class Car {
}
class Tyre { // 一个Java文件可以同时定义多个class
}
class Engine {
}
class Seat {
}
上面的类定义好后,没有任何的其他信息,就跟我们拿到一张张图纸,但是纸上没有任何信息,这是一个空类,没有任何实际意义。所以,我们需要定义类的具体信息。对于一个类来说,一般有三种常见的成员:属性field、方法method、构造器constructor。这三种成员都可以定义零个或多个。
【示例4-2】简单的学生类编写
1
2
3
4
5
6
7
8
9
10
11
12
13 public class SxtStu {
//属性(成员变量)
int id;
String sname;
int age;
//方法
void study(){
System.out.println(“我正在学习!”);
}
//构造方法
SxtStu(){
}
}
4.3.2 属性(field,或者叫成员变量)
属性用于定义该类或该类对象包含的数据或者说静态特征。属性作用范围是整个类体。
在定义成员变量时可以对其初始化,如果不对其初始化,Java使用默认的值对其初始化。
表4-2成员变量的默认值
数据类型 默认值
整型 0
浮点型 0.0
字符型 ‘\u0000’
布尔型 false
所有引用类型 null
属性定义格式:
1 [修饰符] 属性类型 属性名 = [默认值] ;
4.3.3 方法
方法用于定义该类或该类实例的行为特征和功能实现。方法是类和对象行为特征的抽象。方法很类似于面向过程中的函数。面向过程中,函数是最基本单位,整个程序由一个个函数调用组成。面向对象中,整个程序的基本单位是类,方法是从属于类和对象的。
方法定义格式:
[修饰符] 方法返回值类型 方法名(形参列表) {
// n条语句
}
4.3.4 一个典型类的定义和UML图
【示例4-3】模拟学生使用电脑学习
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 class Computer {
String brand; //品牌
}
public class SxtStu {
// field
int id;
String sname;
int age;
Computer comp;
void study() {
System.out.println(“我正在学习!使用我们的电脑,”+comp.brand);
}
SxtStu() {
}
public static void main(String[] args) {
SxtStu stu1 = new SxtStu();
stu1.sname = “张三”;
Computer comp1 = new Computer();
comp1.brand = “联想”;
stu1.comp = comp1;
stu1.study();
}
}
执行结果:
图4-2 示例4-3运行结果
对应的UML图如下:
图4-3 SxtStu和Computer的UML类图
4.4 面向对象的内存分析
为了让大家对于面向对象编程有更深入的了解,我们要对程序的执行过程中,内存到底发生了什么变化进行剖析,让大家做到“心中有数”,通过更加形象方式理解程序的执行方式。
建议:
1. 本节课是为了让初学者更深入了解程序底层执行情况,为了完整的体现内存分析流程,会有些新的名词,比如:线程、Class对象。大家暂时可以不求甚解的了解,后期学了这两个概念再回头来看我们这篇内存分析,肯定收获会更大。
2.学习本节,一定要结合视频学习!
Java虚拟机的内存可以分为三个区域:栈stack、堆heap、方法区method area。
栈的特点如下:
1. 栈描述的是方法执行的内存模型。每个方法被调用都会创建一个栈帧(存储局部变量、操作数、方法出口等)
2. JVM为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数、局部变量等)
3. 栈属于线程私有,不能实现线程间的共享!
4. 栈的存储特性是“先进后出,后进先出”
5. 栈是由系统自动分配,速度快!栈是一个连续的内存空间!
堆的特点如下:
1. 堆用于存储创建好的对象和数组(数组也是对象)
2. JVM只有一个堆,被所有线程共享
3. 堆是一个不连续的内存空间,分配灵活,速度慢!
方法区(又叫静态区)特点如下:
1. JVM只有一个方法区,被所有线程共享!
2. 方法区实际也是堆,只是用于存储类、常量相关的信息!
3. 用来存放程序中永远是不变或唯一的内容。(类信息【Class对象】、静态变量、字符串常量等)
图4-4 示例4-3内存分配图
4.5 构造方法
构造器也叫构造方法(constructor),用于对象的初始化。构造器是一个创建对象时被自动调用的特殊方法,目的是对象的初始化。构造器的名称应与类的名称一致。Java通过new关键字来调用构造器,从而返回该类的实例,是一种特殊的方法。
声明格式:
1
2
3 [修饰符] 类名(形参列表){
//n条语句
}
要点:
1. 通过new关键字调用!!
2. 构造器虽然有返回值,但是不能定义返回值类型(返回值的类型肯定是本类),不能在构造器里使用return返回某个值。
3. 如果我们没有定义构造器,则编译器会自动定义一个无参的构造函数。如果已定义则编译器不会自动添加!
4. 构造器的方法名必须和类名一致!
课堂练习:
1. 定义一个“点”(Point)类用来表示二维空间中的点(有两个坐标)。要求如下:
(1) 可以生成具有特定坐标的点对象。
(2) 提供可以设置坐标的方法。
(3)提供可以计算该“点”距另外一点距离的方法。
参考答案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 class Point {
double x, y;
public Point(double _x, double _y) {
x = _x;
y = _y;
}
public double getDistance(Point p) {
return Math.sqrt((x - p.x) * (x - p.x) + (y - p.y) * (y - p.y));
}
}
public class TestConstructor {
public static void main(String[] args) {
Point p = new Point(3.0, 4.0);
Point origin = new Point(0.0, 0.0);
System.out.println(p.getDistance(origin));
}
}
作业:
1. 很多零基础同学会在这个地方开始晕菜。大家都学过内存分析,将这个程序的执行过程的内存分析画出来。如果画不出,好好再温习一下内存分析那一节课 面向对象内存分析
4.6 构造方法的重载
构造方法也是方法,只不过有特殊的作用而已。与普通方法一样,构造方法也可以重载。
【示例4-6】构造方法重载(创建不同用户对象)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 public class User {
int id; // id
String name; // 账户名
String pwd; // 密码
public User() {
} public User(int id, String name) { super(); this.id = id; this.name = name; } public User(int id, String name, String pwd) { this.id = id; this.name = name; this.pwd = pwd; } public static void main(String[] args) { User u1 = new User(); User u2 = new User(101, "高小七"); User u3 = new User(100, "高淇", "123456"); }
}
雷区:
如果方法构造中形参名与属性名相同时,需要使用this关键字区分属性与形参。如示例4-6所示:
this.id 表示属性id;id表示形参id
4.7 垃圾回收机制(Garbage Collection)
Java引入了垃圾回收机制,令C++程序员最头疼的内存管理问题迎刃而解。Java程序员可以将更多的精力放到业务逻辑上而不是内存管理工作上,大大的提高了开发效率。
4.7.1 垃圾回收原理和算法
·内存管理
Java的内存管理很大程度指的就是对象的管理,其中包括对象空间的分配和释放。
对象空间的分配:使用new关键字创建对象即可
对象空间的释放:将对象赋值null即可。垃圾回收器将负责回收所有”不可达”对象的内存空间。
·垃圾回收过程
任何一种垃圾回收算法一般要做两件基本事情:
1. 发现无用的对象
2. 回收无用对象占用的内存空间。
垃圾回收机制保证可以将“无用的对象”进行回收。无用的对象指的就是没有任何变量引用该对象。Java的垃圾回收器通过相关算法发现无用对象,并进行清除和整理。
·垃圾回收相关算法
1. 引用计数法
堆中每个对象都有一个引用计数。被引用一次,计数加1. 被引用变量值变为null,则计数减1,直到计数为0,则表示变成无用对象。优点是算法简单,缺点是“循环引用的无用对象”无法别识别。
【示例4-7】循环引用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14 public class Student {
String name;
Student friend;
public static void main(String[] args) {
Student s1 = new Student();
Student s2 = new Student();
s1.friend = s2;
s2.friend = s1;
s1 = null;
s2 = null;
}
}
s1和s2互相引用对方,导致他们引用计数不为0,但是实际已经无用,但无法被识别。
2. 引用可达法(根搜索算法)
程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
4.7.2 通用的分代垃圾回收机制
分代垃圾回收机制,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。我们将对象分为三种状态:年轻代、年老代、持久代。JVM将堆内存划分为 Eden、Survivor 和 Tenured/Old 空间。
1. 年轻代
所有新生成的对象首先都是放在Eden区。 年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,对应的是Minor GC,每次 Minor GC 会清理年轻代的内存,算法采用效率较高的复制算法,频繁的操作,但是会浪费内存空间。当“年轻代”区域存放满对象后,就将对象存放到年老代区域。
2. 年老代
在年轻代中经历了N(默认15)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。年老代对象越来越多,我们就需要启动Major GC和Full GC(全量回收),来一次大扫除,全面清理年轻代区域和年老代区域。
3. 持久代
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。
图4-7 堆内存的划分细节
·Minor GC: 用于清理年轻代区域。Eden区满了就会触发一次Minor GC。清理无用对象,将有用对象复制到“Survivor1”、“Survivor2”区中(这两个区,大小空间也相同,同一时刻Survivor1和Survivor2只有一个在用,一个为空)
·Major GC:用于清理老年代区域。
·Full GC:用于清理年轻代、年老代区域。 成本较高,会对系统性能产生影响。
垃圾回收过程:
1、新创建的对象,绝大多数都会存储在Eden中,
2、当Eden满了(达到一定比例)不能创建新对象,则触发垃圾回收(GC),将无用对象清理掉,
然后剩余对象复制到某个Survivor中,如S1,同时清空Eden区
3、当Eden区再次满了,会将S1中的不能清空的对象存到另外一个Survivor中,如S2,
同时将Eden区中的不能清空的对象,也复制到S1中,保证Eden和S1,均被清空。
4、重复多次(默认15次)Survivor中没有被清理的对象,则会复制到老年代Old(Tenured)区中,
5、当Old区满了,则会触发一个一次完整地垃圾回收(FullGC),之前新生代的垃圾回收称为(minorGC)
4.7.3 JVM调优和Full GC
在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:
1.年老代(Tenured)被写满
2.持久代(Perm)被写满
3.System.gc()被显式调用(程序建议GC启动,不是调用GC)
4.上一次GC之后Heap的各域分配策略动态变化
4.7.4 开发中容易造成内存泄露的操作
建议:
1. 在实际开发中,经常会造成系统的崩溃。如下这些操作我们应该注意这些使用场景。 请大家学完相关内容后,回头过来温习下面的内容。不要求此处掌握相关细节。
如下四种情况时最容易造成内存泄露的场景,请大家开发时一定注意:
· 创建大量无用对象
比如,我们在需要大量拼接字符串时,使用了String而不是StringBuilder。
1
2
3
4 String str = “”;
for (int i = 0; i < 10000; i++) {
str += i; //相当于产生了10000个String对象
}
· 静态集合类的使用
像HashMap、Vector、List等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放。
· 各种连接对象(IO流对象、数据库连接对象、网络连接对象)未关闭
IO流对象、数据库连接对象、网络连接对象等连接对象属于物理连接,和硬盘或者网络连接,不使用的时候一定要关闭。
· 监听器的使用
释放对象时,没有删除相应的监听器。
要点:
1. 程序员无权调用垃圾回收器。
2. 程序员可以调用System.gc(),该方法只是通知JVM,并不是运行垃圾回收器。尽量少用,会申请启动Full GC,成本高,影响系统性能。
3. finalize方法,是Java提供给程序员用来释放对象或资源的方法,但是尽量少用。
4.8 this关键字
· 对象创建的过程和this的本质
构造方法是创建Java对象的重要途径,通过new关键字调用构造器时,构造器也确实返回该类的对象,但这个对象并不是完全由构造器负责创建。创建一个对象分为如下四步:
1. 分配对象空间,并将对象成员变量初始化为0或空
2. 执行属性值的显示初始化
3. 执行构造方法
4. 返回对象的地址给相关的变量
this的本质就是“创建好的对象的地址”! 由于在构造方法调用前,对象已经创建。因此,在构造方法中也可以使用this代表“当前对象” 。
this最常的用法:
1. 在程序中产生二义性之处,应使用this来指明当前对象;普通方法中,this总是指向调用该方法的对象。构造方法中,this总是指向正要初始化的对象。
2. 使用this关键字调用重载的构造方法,避免相同的初始化代码。但只能在构造方法中用,并且必须位于构造方法的第一句。
3. this不能用于static方法中。
【示例4-8】this代表“当前对象”示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 public class User {
int id; //id
String name; //账户名
String pwd; //密码
public User() { } public User(int id, String name) { System.out.println("正在初始化已经创建好的对象:"+this); this.id = id; //不写this,无法区分局部变量id和成员变量id this.name = name; } public void login(){ System.out.println(this.name+",要登录!"); //不写this效果一样 } public static void main(String[] args) { User u3 = new User(101,"高小七"); System.out.println("打印高小七对象:"+u3); u3.login(); }
}
运行结果如图4-8所示。
图4-8 示例4-8运行结果
【示例4-9】this()调用重载构造方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 public class TestThis {
int a, b, c;
TestThis() {
System.out.println("正要初始化一个Hello对象");
}
TestThis(int a, int b) {
// TestThis(); //这样是无法调用构造方法的!
this(); // 调用无参的构造方法,并且必须位于第一行!
a = a;// 这里都是指的局部变量而不是成员变量
// 这样就区分了成员变量和局部变量. 这种情况占了this使用情况大多数!
this.a = a;
this.b = b;
}
TestThis(int a, int b, int c) {
this(a, b); // 调用带参的构造方法,并且必须位于第一行!
this.c = c;
}
void sing() {
}
void eat() {
this.sing(); // 调用本类中的sing();
System.out.println("你妈妈喊你回家吃饭!");
}
public static void main(String[] args) {
TestThis hi = new TestThis(2, 3);
hi.eat();
}
}
4.9 static 关键字
在类中,用static声明的成员变量为静态成员变量,也称为类变量。 类变量的生命周期和类相同,在整个应用程序执行期间都有效。它有如下特点:
1. 为该类的公用变量,属于类,被该类的所有实例共享,在类被载入时被显式初始化。
2. 对于该类的所有对象来说,static成员变量只有一份。被该类的所有对象共享!!
3. 一般用“类名.类属性/方法”来调用。(也可以通过对象引用或类名(不需要实例化)访问静态成员。)
4. 在static方法中不可直接访问非static的成员。
核心要点:
static修饰的成员变量和方法,从属于类。
普通变量和方法从属于对象的。
【示例4-10】static关键字的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 /**
*/
public class User2 {
int id; // id
String name; // 账户名
String pwd; // 密码
static String company = "北京尚学堂"; // 公司名称
public User2(int id, String name) {
this.id = id;
this.name = name;
}
public void login() {
printCompany();
System.out.println(company);
System.out.println("登录:" + name);
}
public static void printCompany() {
// login();//调用非静态成员,编译就会报错
System.out.println(company);
}
public static void main(String[] args) {
User2 u = new User2(101, "高小七");
User2.printCompany();
User2.company = "北京阿里爷爷";
User2.printCompany();
}
}
运行结果如图4-9所示。
图4-10 示例4-10的内存分配图
4.10 静态初始化块
构造方法用于对象的初始化!静态初始化块,用于类的初始化操作!在静态初始化块中不能直接访问非static成员。
注意事项:
静态初始化块执行顺序(学完继承再看这里):
1. 上溯到Object类,先执行Object的静态初始化块,再向下执行子类的静态初始化块,直到我们的类的静态初始化块为止。
2. 构造方法执行顺序和上面顺序一样!!
【示例4-11】static初始化块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 public class User3 {
int id; //id
String name; //账户名
String pwd; //密码
static String company; //公司名称
static {
System.out.println(“执行类的初始化工作”);
company = “北京尚学堂”;
printCompany();
}
public static void printCompany(){
System.out.println(company);
}
public static void main(String[] args) {
User3 u3 = new User3();
}
}
执行结果如图4-11所示。
图4-11 示例4-11的运行结果
4.11 参数传值机制
Java中,方法中所有参数都是“值传递”,也就是“传递的是值的副本”。 也就是说,我们得到的是“原参数的复印件,而不是原件”。因此,复印件改变不会影响原件。
· 基本数据类型参数的传值
传递的是值的副本。 副本改变不会影响原件。
· 引用类型参数的传值
传递的是值的副本。但是引用类型指的是“对象的地址”。因此,副本和原参数都指向了同一个“地址”,改变“副本指向地址对象的值,也意味着原参数指向对象的值也发生了改变”。
【示例4-12】 多个变量指向同一个对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 /**
*/
public class User4 {
int id; //id
String name; //账户名
String pwd; //密码
public User4(int id, String name) { this.id = id; this.name = name; } public void testParameterTransfer01(User4 u){ u.name="高小八"; } public void testParameterTransfer02(User4 u){ u = new User4(200,"高三"); } public static void main(String[] args) { User4 u1 = new User4(100, "高小七"); u1.testParameterTransfer01(u1); System.out.println(u1.name); u1.testParameterTransfer02(u1); System.out.println(u1.name); }
}
执行结果如图4-12所示。
图4-12 示例4-12的运行结果
4.13 包
包机制是Java中管理类的重要手段。 开发中,我们会遇到大量同名的类,通过包我们很容易对解决类重名的问题,也可以实现对类的有效管理。 包对于类,相当于文件夹对于文件的作用。
4.13 package
我们通过package实现对类的管理,package的使用有两个要点:
1. 通常是类的第一句非注释性语句。
2. 包名:域名倒着写即可,再加上模块名,便于内部管理类。
【示例4-13】package的命名举例
1
2
3
4
5 com.sun.test;
com.oracle.test;
cn.sxt.gao.test;
cn.sxt.gao.view;
cn.sxt.gao.view.model;
注意事项:
1. 写项目时都要加包,不要使用默认包。
2. com.gao和com.gao.car,这两个包没有包含关系,是两个完全独立的包。只是逻辑上看起来后者是前者的一部分。
【示例4-14】package的使用
1
2
3
4
5
6 package cn.sxt;
public class Test {
public static void main(String[] args) {
System.out.println(“helloworld”);
}
}
·在eclipse项目中新建包
在src目录上单击右键,选择new->package
图4-13 创建package
在package窗口上输入包名即可
图4-14 指定包名称
接下来,我们就可以在包上单击右键,新建类啦!
4.13.1 JDK中的主要包
表4-3 JDK中的主要包
Java中的常用包 说明
java.lang 包含一些Java语言的核心类,如String、Math、Integer、System和Thread,提供常用功能。
java.awt 包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。
java.net 包含执行与网络相关的操作的类。
java.io 包含能提供多种输入/输出功能的类。
java.util 包含一些实用工具类,如定义系统特性、使用与日期日历相关的函数。
4.13.2 导入类import
如果我们要使用其他包的类,需要使用import导入,从而可以在本类中直接通过类名来调用,否则就需要书写类的完整包名和类名。import后,便于编写代码,提高可维护性。
注意要点:
1. Java会默认导入java.lang包下所有的类,因此这些类我们可以直接使用。
2. 如果导入两个同名的类,只能用包名+类名来显示调用相关类:
1 java.util.Date date = new java.util.Date();
【示例4-15】导入同名类的处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14 import java.sql.Date;
import java.util.*;//导入该包下所有的类。会降低编译速度,但不会降低运行速度。
public class Test{
public static void main(String[] args) {
//这里指的是java.sql.Date
Date now;
//java.util.Date因为和java.sql.Date类同名,需要完整路径
java.util.Date now2 = new java.util.Date();
System.out.println(now2);
//java.util包的非同名类不需要完整路径
Scanner input = new Scanner(System.in);
}
}
4.13.3 静态导入
静态导入(static import)是在JDK1.5新增加的功能,其作用是用于导入指定类的静态属性,这样我们可以直接使用静态属性。
【示例4-16】静态导入的使用
1
2
3
4
5
6
7
8
9
10
11 package cn.sxt;
//以下两种静态导入的方式二选一即可
import static java.lang.Math.*;//导入Math类的所有静态属性
import static java.lang.Math.PI;//导入Math类的PI属性
public class Test2{
public static void main(String [] args){
System.out.println(PI);
System.out.println(random());
}
}
执行结果如图4-15所示。
图4-15 示例4-16运行结果
第四章 总结
1. 面向对象可以帮助我们从宏观上把握、从整体上分析整个系统。 但是具体到实现部分的微观操作(就是一个个方法),仍然需要面向过程的思路去处理。
2. 类可以看成一类对象的模板,对象可以看成该类的一个具体实例。
3. 对于一个类来说,一般有三种常见的成员:属性field、方法method、构造器constructor。
4. 构造器也叫构造方法,用于对象的初始化。构造器是一个创建对象时被自动调用的特殊方法,目的是对象的初始化。构造器的名称应与类的名称一致。
5. Java引入了垃圾回收机制,令C++程序员最头疼的内存管理问题迎刃而解。Java程序员可将更多的精力放到业务逻辑上而不是内存管理工作,大大提高开发效率。
6. this的本质就是“创建好的对象的地址”! this不能用于static方法中。
7. 在类中,用static声明的成员变量为静态成员变量,也称为类变量。类变量的生命周期和类相同,在整个应用程序执行期间都有效。在static方法中不可直接访问非static的成员。
8. Java方法中所有参数都是“值传递”,也就是“传递的是值的副本”。也就是说,我们得到的是“原参数的复印件,而不是原件”。因此,复印件改变不会影响原件。
9. 通过package实现对类的管理;如果我们要使用其他包的类,需要使用import导入,从而可以在本类中直接通过类名来调用。
第四章 作业
一、选择题
1.以下语句中关于Java构造方法的说法错误的是( )。(选择一项) B
A.构造方法的作用是为创建对象进行初始化工作,比如给成员变量赋值
B.一个Java类可以没有构造方法,也可以提供1个或多个构造方法
C.构造方法与类同名,不能书写返回值类型
D.构造方法的第一条语句如果是super(),则可以省略,该语句作用是调用父类无参数的构造方法
2.在Java中,以下程序编译运行后的输出结果为( )。(选择一项) D
1
2
3
4
5
6
7
8
9
10
11
12
13 public class Test {
int x, y;
Test(int x, int y) {
this.x = x;
this.y = y;
}
public static void main(String[] args) {
Test pt1, pt2;
pt1 = new Test(3, 3);
pt2 = new Test(4, 4);
System.out.print(pt1.x + pt2.x);
}
}
A.6
B.34
C.8
D.7
3.在Java中关于静态方法,以下说法中正确的是( )。(选择二项)AC
A.静态方法中不能直接调用非静态方法
B.非静态方法中不能直接调用静态方法
C.静态方法可以用类名直接调用
D.静态方法里可以使用this
4.下列选项中关于Java中类方法的说法错误的是( )。(选择二项)AC
A.在类方法中可用this来调用本类的类方法
B.在类方法中调用本类的类方法时可直接调用
C.在类方法中只能调用本类中的类方法
D.在类方法中调用实例方法需要先创建对象
5.分析如下Java程序的代码所示,则编译运行后的输出结果是( )。(选择一项) C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 public class Test {
int count=9;
public void count1(){
count=10;
System.out.print(“count1=”+count);
}
public void count2(){
System.out.print(“count2=”+count);
}
public static void main(String[ ] args) {
Test t=new Test();
t.count1();
t.count2();
}
}
A.count1=9; count2=9;
B.count1=10;count2=9;
C.count1=10; count2=10;
D.count1=9; count2=10;
二、简答题
三、编码题
编写 Java 程序用于显示人的姓名和年龄。定义一个人类Person。 该类中应该有两个私有属性: 姓名 (name) 和年龄 (age) 。定义构造方法用来初始化数据成员。再定义显示(display()) 方法将姓名和年龄打印出来。在 main 方法中创建人类的实例然后将信息显示。
class Person{
private String name;
private int age;
public Person() {
name = "Kurt";
age = 18;
}
public Person(String name,int age) {
this();
this.name = name;
this.age = age;
}
public void display() {
System.out.println("姓名:"+this.name);
System.out.println("年龄:"+this.age);
}
}
public class Fourth1 {
public static void main(String[] args) {
Person p = new Person();
p.display();
}
}
定义一个圆类——Circle,在类的内部提供一个属性:半径®,同时 提供 两个 方 法 : 计算 面积 ( getArea() ) 和 计算 周长(getPerimeter()) 。 通过两个方法计算圆的周长和面积并且对计算结果进行输出。最后定义一个测试类对 Circle 类进行使用。
class Circle{
private float r;
public Circle() {
this.r= 5.0f;
}
public void getArea() {
System.out.println("圆的面积为:"+Math.PI*r*r);
}
public void getPerimeter() {
System.out.println("圆的周长为:"+2*Math.PI*r);
}
public void getR() {
System.out.println(r);
}
}
public class Fourth2 {
public static void main(String[] args) {
Circle c = new Circle();
c.getArea();
c.getPerimeter();
}
}
3. 构造方法与重载:定义一个网络用户类,信息有用户 ID、用户密码、 email 地址。在建立类的实例时把以上三个信息都作为构造函数的参数输入, 其中用户 ID 和用户密码时必须缺省时 email地址是用户 ID 加上字符串"@gameschool.com"。
class User{
private String id;
private String password;
private String email;
public User(String id, String password,String email) {
this.id = id;
this.password = password;
this.email = email;
}
public User(String id,String password) {
this.id = id;
this.password = password;
this.email = id+"@gameschool.com";
}
}
public class Fourth3 {
public static void main(String[] args) {
User u = new User(“Kurt”, “123”);
}
}
第五章 Java面向对象进阶
5.1 概述
本章重点针对面向对象的三大特征:继承、封装、多态进行详细的讲解。另外还包括抽象类、接口、内部类等概念。很多概念对于初学者来说,更多的是先进行语法性质的了解。不要期望,通过本章学习就“搞透面向对象”。本章只是面向对象的起点,后面所有的章节说白了都是对面向对象这一章的应用。
5.1.1 继承的实现
继承让我们更加容易实现类的扩展。 比如,我们定义了人类,再定义Boy类就只需要扩展人类即可。实现了代码的重用,不用再重新发明轮子(don’t reinvent wheels)。
从英文字面意思理解,extends的意思是“扩展”。子类是父类的扩展。现实世界中的继承无处不在。比如:
图5-1 现实世界中的继承
上图中,哺乳动物继承了动物。意味着,动物的特性,哺乳动物都有;在我们编程中,如果新定义一个Student类,发现已经有Person类包含了我们需要的属性和方法,那么Student类只需要继承Person类即可拥有Person类的属性和方法。
【示例5-1】使用extends实现继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 public class Test{
public static void main(String[] args) {
Student s = new Student(“高淇”,172,“Java”);
s.rest();
s.study();
}
}
class Person {
String name;
int height;
public void rest(){
System.out.println(“休息一会!”);
}
}
class Student extends Person {
String major; //专业
public void study(){
System.out.println(“在尚学堂,学习Java”);
}
public Student(String name,int height,String major) {
//天然拥有父类的属性
this.name = name;
this.height = height;
this.major = major;
}
}
执行结果如图5-2所示:
图5-2 示例5-1运行效果图
5.1.2 instanceof 运算符
instanceof是二元运算符,左边是对象,右边是类;当对象是右面类或子类所创建对象时,返回true;否则,返回false。比如:
【示例5-2】使用instanceof运算符进行类型判断
1
2
3
4
5
6
7 public class Test{
public static void main(String[] args) {
Student s = new Student(“高淇”,172,“Java”);
System.out.println(s instanceof Person);
System.out.println(s instanceof Student);
}
}
两条语句的输出结果都是true。
5.1.3 继承使用要点
1.父类也称作超类、基类、派生类等。
2.Java中只有单继承,没有像C++那样的多继承。多继承会引起混乱,使得继承链过于复杂,系统难于维护。
3.Java中类没有多继承,接口有多继承。
4.子类继承父类,可以得到父类的全部属性和方法 (除了父类的构造方法),但不见得可以直接访问(比如,父类私有的属性和方法)。
5.如果定义一个类时,没有调用extends,则它的父类是:java.lang.Object。
5.1.4方法的重写override
子类通过重写父类的方法,可以用自身的行为替换父类的行为。方法的重写是实现多态的必要条件。
方法的重写需要符合下面的三个要点:
1.“==”: 方法名、形参列表相同。
2.“≤”:返回值类型和声明异常类型,子类小于等于父类。
3.“≥”: 访问权限,子类大于等于父类。
【示例5-3】方法重写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 public class TestOverride {
public static void main(String[] args) {
Vehicle v1 = new Vehicle();
Vehicle v2 = new Horse();
Vehicle v3 = new Plane();
v1.run();
v2.run();
v3.run();
v2.stop();
v3.stop();
}
}
class Vehicle { // 交通工具类
public void run() {
System.out.println(“跑…”);
}
public void stop() {
System.out.println(“停止不动”);
}
}
class Horse extends Vehicle { // 马也是交通工具
public void run() { // 重写父类方法
System.out.println(“四蹄翻飞,嘚嘚嘚…”);
}
}
class Plane extends Vehicle {
public void run() { // 重写父类方法
System.out.println(“天上飞!”);
}
public void stop() {
System.out.println(“空中不能停,坠毁了!”);
}
}
执行结果如图5-3所示:
图5-3 示例5-3运行效果图
5.2.1 Object类基本特性
Object类是所有Java类的根基类,也就意味着所有的Java对象都拥有Object类的属性和方法。如果在类的声明中未使用extends关键字指明其父类,则默认继承Object类。
【示例5-4】Object类
1
2
3
4
5
6
7 public class Person {
…
}
//等价于:
public class Person extends Object {
…
}
5.2.2 toString方法
Object类中定义有public String toString()方法,其返回值是 String 类型。Object类中toString方法的源码为:
1
2
3 public String toString() {
return getClass().getName() + “@” + Integer.toHexString(hashCode());
}
根据如上源码得知,默认会返回“类名+@+16进制的hashcode”。在打印输出或者用字符串连接对象时,会自动调用该对象的toString()方法。
【示例5-5】toString()方法测试和重写toString()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 class Person {
String name;
int age;
@Override
public String toString() {
return name+“,年龄:”+age;
}
}
public class Test {
public static void main(String[] args) {
Person p=new Person();
p.age=20;
p.name=“李东”;
System.out.println(“info:”+p);
Test t = new Test();
System.out.println(t);
}
}
执行结果如图5-4所示:
图5-4 示例5-5运行效果图
5.2.3 和equals方法
“”代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表示地址相等即是同一个对象。
Object类中定义有:public boolean equals(Object obj)方法,提供定义“对象内容相等”的逻辑。比如,我们在公安系统中认为id相同的人就是同一个人、学籍系统中认为学号相同的人就是同一个人。
Object 的 equals 方法默认就是比较两个对象的hashcode,是同一个对象的引用时返回 true 否则返回 false。但是,我们可以根据我们自己的要求重写equals方法。
【示例5-6】equals方法测试和自定义类重写equals方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 public class TestEquals {
public static void main(String[] args) {
Person p1 = new Person(123,“高淇”);
Person p2 = new Person(123,“高小七”);
System.out.println(p1p2); //false,不是同一个对象
System.out.println(p1.equals(p2)); //true,id相同则认为两个对象内容相同
String s1 = new String(“尚学堂”);
String s2 = new String(“尚学堂”);
System.out.println(s1s2); //false, 两个字符串不是同一个对象
System.out.println(s1.equals(s2)); //true, 两个字符串内容相同
}
}
class Person {
int id;
String name;
public Person(int id,String name) {
this.id=id;
this.name=name;
}
public boolean equals(Object obj) {
if(obj == null){
return false;
}else {
if(obj instanceof Person) {
Person c = (Person)obj;
if(c.id==this.id) {
return true;
}
}
}
return false;
}
}
JDK提供的一些类,如String、Date、包装类等,重写了Object的equals方法,调用这些类的equals方法, x.equals (y) ,当x和y所引用的对象是同一类对象且属性内容相等时(并不一定是相同对象),返回 true 否则返回 false。
5.3 super关键字
super是直接父类对象的引用。可以通过super来访问父类中被子类覆盖的方法或属性。
使用super调用普通方法,语句没有位置限制,可以在子类中随便调用。
若是构造方法的第一行代码没有显式的调用super(…)或者this(…);那么Java默认都会调用super(),含义是调用父类的无参数构造方法。这里的super()可以省略。
【示例5-7】super关键字的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 public class TestSuper01 {
public static void main(String[] args) {
new ChildClass().f();
}
}
class FatherClass {
public int value;
public void f(){
value = 100;
System.out.println (“FatherClass.value=”+value);
}
}
class ChildClass extends FatherClass {
public int value;
public void f() {
super.f(); //调用父类对象的普通方法
value = 200;
System.out.println(“ChildClass.value=”+value);
System.out.println(value);
System.out.println(super.value); //调用父类对象的成员变量
}
}
执行结果如图5-5所示:
图5-5 示例5-7运行效果图
5.3.1 继承树追溯
·属性/方法查找顺序:(比如:查找变量h)
1. 查找当前类中有没有属性h
2. 依次上溯每个父类,查看每个父类中是否有h,直到Object
3. 如果没找到,则出现编译错误。
4. 上面步骤,只要找到h变量,则这个过程终止。
·构造方法调用顺序:
构造方法第一句总是:super(…)来调用父类对应的构造方法。所以,流程就是:先向上追溯到Object,然后再依次向下执行类的初始化块和构造方法,直到当前子类为止。
注:静态初始化块调用顺序,与构造方法调用顺序一样,不再重复。
【示例5-8】构造方法向上追溯执行测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 public class TestSuper02 {
public static void main(String[] args) {
System.out.println(“开始创建一个ChildClass对象…”);
new ChildClass();
}
}
class FatherClass {
public FatherClass() {
System.out.println(“创建FatherClass”);
}
}
class ChildClass extends FatherClass {
public ChildClass() {
System.out.println(“创建ChildClass”);
}
}
执行结果如图5-6所示:
图5-6 示例5-8运行效果图
5.4.1 封装的作用和含义
我要看电视,只需要按一下开关和换台就可以了。有必要了解电视机内部的结构吗?有必要碰碰显像管吗?制造厂家为了方便我们使用电视,把复杂的内部细节全部封装起来,只给我们暴露简单的接口,比如:电源开关。具体内部是怎么实现的,我们不需要操心。
需要让用户知道的才暴露出来,不需要让用户知道的全部隐藏起来,这就是封装。
说的专业一点,封装就是把对象的属性和操作结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。
我们程序设计要追求“高内聚,低耦合”。
高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;
低耦合是仅暴露少量的方法给外部使用,尽量方便外部调用。
编程中封装的具体优点:
1. 提高代码的安全性。
2. 提高代码的复用性。
3. “高内聚”:封装细节,便于修改内部代码,提高可维护性。
4. “低耦合”:简化外部调用,便于调用者使用,便于扩展和协作。
【示例5-9】没有封装的代码会出现一些问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 class Person {
String name;
int age;
@Override
public String toString() {
return “Person [name=” + name + “, age=” + age + “]”;
}
}
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.name = “小红”;
p.age = -45;//年龄可以通过这种方式随意赋值,没有任何限制
System.out.println§;
}
}
我们都知道,年龄不可能是负数,也不可能超过130岁,但是如果没有使用封装的话,便可以给年龄赋值成任意的整数,这显然不符合我们的正常逻辑思维。执行结果如图5-7所示:
图5-7 示例5-9运行效果图
再比如说,如果哪天我们需要将Person类中的age属性修改为String类型的,你会怎么办?你只有一处使用了这个类的话那还比较幸运,但如果你有几十处甚至上百处都用到了,那你岂不是要改到崩溃。而封装恰恰能解决这样的问题。如果使用封装,我们只需要稍微修改下Person类的setAge()方法即可,而无需修改使用了该类的客户代码。
5.4.2 封装的实现—使用访问控制符
Java是使用“访问控制符”来控制哪些细节需要封装,哪些细节需要暴露的。
Java中4种“访问控制符”分别为private、default、protected、public,
它们说明了面向对象的封装性,所以我们要利用它们尽可能的让访问权限降到最低,从而提高安全性。
下面详细讲述它们的访问权限问题。其访问权限范围如表5-1所示。
表5-1 访问权限修饰符
1. private 表示私有,只有自己类能访问
2. default表示没有修饰符修饰,只有同一个包的类能访问
3. protected表示可以被同一个包的类以及其他包中的子类访问
4. public表示可以被该项目的所有包中的所有类访问
下面做进一步说明Java中4种访问权限修饰符的区别:首先我们创建4个类:Person类、Student类、Animal类和Computer类,分别比较本类、本包、子类、其他包的区别。
public访问权限修饰符:
图5-8 public访问权限—本类中访问public属性
图5-9 public访问权限—本包中访问public属性
图5-10 public访问权限—不同包中的子类访问public属性
图5-11 public访问权限—不同包中的非子类访问public属性
通过图5-8 ~ 图5-11可以说明,public修饰符的访问权限为:该项目的所有包中的所有类。
protected访问权限修饰符:将Person类中属性改为protected,其他类不修改。
图5-12 protected访问权限—修改后的Person类
图5-13 protected访问权限—不同包中的非子类不能访问protected属性
通过图5-12和图5-13可以说明,protected修饰符的访问权限为:同一个包中的类以及其他包中的子类。
默认访问权限修饰符:将Person类中属性改为默认的,其他类不修改。
图5-14 默认访问权限—修改后的Person类
通过图5-14可以说明,默认修饰符的访问权限为:同一个包中的类。
private访问权限修饰符:将Person类中属性改为private,其他类不修改。
图5-15 private访问权限—修改后的Person类
通过图5-15可以说明,private修饰符的访问权限为:同一个类。
5.4.3 封装的使用细节
类的属性的处理:
1. 一般使用private访问权限。(同一个类)
2. 提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的,以提供对属性的赋值与读取操作(注意:boolean变量的get方法是is开头!)。
3. 一些只用于本类的辅助性方法可以用private修饰,希望其他类调用的方法用public修饰。
【示例5-10】JavaBean的封装实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 public class Person {
// 属性一般使用private修饰
private String name;
private int age;
private boolean flag;
// 为属性提供public修饰的set/get方法
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 boolean isFlag() {// 注意:boolean类型的属性get方法是is开头的
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
下面我们使用封装来解决一下5.4.1中提到的年龄非法赋值的问题。
【示例5-11】封装的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 class Person {
private String name;
private int age;
public Person() {
} public Person(String name, int age) { this.name = name; // this.age = age;//构造方法中不能直接赋值,应该调用setAge方法 setAge(age); } public void setName(String name) { this.name = name; } public String getName() { return name; } public void setAge(int age) { //在赋值之前先判断年龄是否合法 if (age > 130 || age < 0) { this.age = 18;//不合法赋默认值18 } else { this.age = age;//合法才能赋值给属性age } } public int getAge() { return age; } @Override public String toString() { return "Person [name=" + name + ", age=" + age + "]"; }
}
public class Test2 {
public static void main(String[] args) {
Person p1 = new Person();
//p1.name = “小红”; //编译错误
//p1.age = -45; //编译错误
p1.setName(“小红”);
p1.setAge(-45);
System.out.println(p1);
Person p2 = new Person("小白", 300);
System.out.println(p2);
}
}
执行结果如图5-16所示:
图5-16 示例5-11运行效果图
5.5 多态(polymorphism)
多态指的是同一个方法调用,由于对象不同可能会有不同的行为。现实生活中,同一个方法,具体实现会完全不同。 比如:同样是调用人的“休息”方法,张三是睡觉,李四是旅游,高淇老师是敲代码,数学教授是做数学题; 同样是调用人“吃饭”的方法,中国人用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。
多态的要点:
1. 多态是方法的多态,不是属性的多态(多态与属性无关)。
2. 多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象。
3. 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。
【示例5-12】多态和类型转换测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46 class Animal {
public void shout() {
System.out.println(“叫了一声!”);
}
}
class Dog extends Animal {
public void shout() {
System.out.println(“旺旺旺!”);
}
public void seeDoor() {
System.out.println(“看门中…”);
}
}
class Cat extends Animal {
public void shout() {
System.out.println(“喵喵喵喵!”);
}
}
public class TestPolym {
public static void main(String[] args) {
Animal a1 = new Cat(); // 向上可以自动转型
//传的具体是哪一个类就调用哪一个类的方法。大大提高了程序的可扩展性。
animalCry(a1);
Animal a2 = new Dog();
animalCry(a2);//a2为编译类型,Dog对象才是运行时类型。
//编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。 // 否则通不过编译器的检查。 Dog dog = (Dog)a2;//向下需要强制类型转换 dog.seeDoor(); } // 有了多态,只需要让增加的这个类继承Animal类就可以了。 static void animalCry(Animal a) { a.shout(); } /* 如果没有多态,我们这里需要写很多重载的方法。 * 每增加一种动物,就需要重载一种动物的喊叫方法。非常麻烦。 static void animalCry(Dog d) { d.shout(); } static void animalCry(Cat c) { c.shout(); }*/
}
执行结果如图5-17所示:
图5-17 示例5-12运行效果图
示例5-12给大家展示了多态最为多见的一种用法,即父类引用做方法的形参,实参可以是任意的子类对象,可以通过不同的子类对象实现不同的行为方式。
由此,我们可以看出多态的主要优势是提高了代码的可扩展性,符合开闭原则。但是多态也有弊端,就是无法调用子类特有的功能,比如,我不能使用父类的引用变量调用Dog类特有的seeDoor()方法。
那如果我们就想使用子类特有的功能行不行呢?行!这就是我们下一章节所讲的内容:对象的转型。
5.6 对象的转型(casting)
父类引用指向子类对象,我们称这个过程为向上转型,属于自动类型转换。
向上转型后的父类引用变量只能调用它编译类型的方法,不能调用它运行时类型的方法。这时,我们就需要进行类型的强制转换,我们称之为向下转型!
【示例5-13】对象的转型
1
2
3
4
5
6
7
8
9
10
11 public class TestCasting {
public static void main(String[] args) {
Object obj = new String(“北京尚学堂”); // 向上可以自动转型
// obj.charAt(0) 无法调用。编译器认为obj是Object类型而不是String类型
/* 编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
* 不然通不过编译器的检查。 */
String str = (String) obj; // 向下转型
System.out.println(str.charAt(0)); // 位于0索引位置的字符
System.out.println(obj == str); // true.他们俩运行时是同一个对象
}
}
执行结果如果5-18所示:
图5-18 示例5-13运行效果图
在向下转型过程中,必须将引用变量转成真实的子类类型(运行时类型)否则会出现类型转换异常ClassCastException。如示例5-14所示。
【示例5-14】类型转换异常
1
2
3
4
5
6
7
8 public class TestCasting2 {
public static void main(String[] args) {
Object obj = new String(“北京尚学堂”);
//真实的子类类型是String,但是此处向下转型为StringBuffer
StringBuffer str = (StringBuffer) obj;
System.out.println(str.charAt(0));
}
}
执行结果如果5-19所示:
图5-19 示例5-14运行效果图
为了避免出现这种异常,我们可以使用5.1.2中所学的instanceof运算符进行判断,如示例5-15所示。
【示例5-15】向下转型中使用instanceof
1
2
3
4
5
6
7
8
9
10
11
12 public class TestCasting3 {
public static void main(String[] args) {
Object obj = new String(“北京尚学堂”);
if(obj instanceof String){
String str = (String)obj;
System.out.println(str.charAt(0));
}else if(obj instanceof StringBuffer){
StringBuffer str = (StringBuffer) obj;
System.out.println(str.charAt(0));
}
}
}
执行结果如果5-20所示:
图5-20 示例5-15运行效果图
5.7 final关键字
final关键字的作用:
1. 修饰变量: 被他修饰的变量不可改变。一旦赋了初值,就不能被重新赋值。
1 final int MAX_SPEED = 120;
2. 修饰方法:该方法不可被子类重写。但是可以被重载!
1 final void study(){}
3. 修饰类: 修饰的类不能被继承。比如:Math、String等。
1 final class A {}
final修饰变量详见第二章示例2-9。
final修饰方法如图5-21所示。
图5-21 final修饰方法
final修饰类如图5-22所示。
图5-22 final修饰类
5.8 抽象方法和抽象类
·抽象方法
使用abstract修饰的方法,没有方法体,只有声明。定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体的实现。
·抽象类
包含抽象方法的类就是抽象类。通过abstract方法定义规范,然后要求子类必须定义具体实现。通过抽象类,我们就可以做到严格限制子类的设计,使子类之间更加通用。
【示例5-16】抽象类和抽象方法的基本用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 //抽象类
abstract class Animal {
abstract public void shout(); //抽象方法
}
class Dog extends Animal {
//子类必须实现父类的抽象方法,否则编译错误
public void shout() {
System.out.println(“汪汪汪!”);
}
public void seeDoor(){
System.out.println(“看门中…”);
}
}
//测试抽象类
public class TestAbstractClass {
public static void main(String[] args) {
Dog a = new Dog();
a.shout();
a.seeDoor();
}
}
抽象类的使用要点:
1. 有抽象方法的类只能定义成抽象类
2. 抽象类不能实例化,即不能用new来实例化抽象类。
3. 抽象类可以包含属性、方法、构造方法。但是构造方法不能用来new实例,只能用来被子类调用。
4. 抽象类只能用来被继承。
5. 抽象方法必须被子类实现。
5.9.1接口的作用
接口就是比“抽象类”还“抽象”的“抽象类”,可以更加规范的对子类进行约束。全面地专业地实现了:规范和具体实现的分离。
抽象类还提供某些具体实现,接口不提供任何实现,接口中所有方法都是抽象方法。接口是完全面向规范的,规定了一批类具有的公共方法规范。
从接口的实现者角度看,接口定义了可以向外部提供的服务。
从接口的调用者角度看,接口定义了实现者能提供那些服务。
接口是两个模块之间通信的标准,通信的规范。如果能把你要设计的模块之间的接口定义好,就相当于完成了系统的设计大纲,剩下的就是添砖加瓦的具体实现了。大家在工作以后,做系统时往往就是使用“面向接口”的思想来设计系统。
接口和实现类不是父子关系,是实现规则的关系。比如:我定义一个接口Runnable,Car实现它就能在地上跑,Train实现它也能在地上跑,飞机实现它也能在地上跑。就是说,如果它是交通工具,就一定能跑,但是一定要实现Runnable接口。
· 接口的本质探讨
接口就是规范,定义的是一组规则,体现了现实世界中“如果你是…则必须能…”的思想。如果你是天使,则必须能飞。如果你是汽车,则必须能跑。如果你是好人,则必须能干掉坏人;如果你是坏人,则必须欺负好人。
接口的本质是契约,就像我们人间的法律一样。制定好后大家都遵守。
面向对象的精髓,是对对象的抽象,最能体现这一点的就是接口。为什么我们讨论设计模式都只针对具备了抽象能力的语言(比如C++、Java、C#等),就是因为设计模式所研究的,实际上就是如何合理的去抽象。
区别
1. 普通类:具体实现
2. 抽象类:具体实现,规范(抽象方法)
3. 接口:规范!
5.9.2 如何定义和使用接口?
声明格式:
1
2
3
4 [访问修饰符] interface 接口名 [extends 父接口1,父接口2…] {
常量定义;
方法定义;
}
定义接口的详细说明:
1. 访问修饰符:只能是public或默认。
2. 接口名:和类名采用相同命名机制。
3. extends:接口可以多继承。
4. 常量:接口中的属性只能是常量,总是:public static final 修饰。不写也是。
5. 方法:接口中的方法只能是:public abstract。 省略的话,也是public abstract。
要点
1. 子类通过implements来实现接口中的规范。
2. 接口不能创建实例,但是可用于声明引用变量类型。
3. 一个类实现了接口,必须实现接口中所有的方法,并且这些方法只能是public的。
4. JDK1.7之前,接口中只能包含静态常量、抽象方法,不能有普通属性、构造方法、普通方法。
5. JDK1.8后,接口中包含普通的静态方法。
【示例5-17】接口的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 public class TestInterface {
public static void main(String[] args) {
Volant volant = new Angel();
volant.fly();
System.out.println(Volant.FLY_HIGHT);
Honest honest = new GoodMan();
honest.helpOther();
}
}
/*飞行接口/
interface Volant {
int FLY_HIGHT = 100; // 总是:public static final类型的;
void fly(); //总是:public abstract void fly();
}
/*善良接口/
interface Honest {
void helpOther();
}
/*Angle类实现飞行接口和善良接口/
class Angel implements Volant, Honest{
public void fly() {
System.out.println(“我是天使,飞起来啦!”);
}
public void helpOther() {
System.out.println(“扶老奶奶过马路!”);
}
}
class GoodMan implements Honest {
public void helpOther() {
System.out.println(“扶老奶奶过马路!”);
}
}
class BirdMan implements Volant {
public void fly() {
System.out.println(“我是鸟人,正在飞!”);
}
}
执行结果如果5-23所示:
图5-23 示例5-17运行效果图
5.9.3 接口的多继承
接口完全支持多继承。和类的继承类似,子接口扩展某个父接口,将会获得父接口中所定义的一切。
【示例5-18】接口的多继承
5.9.4 面向接口编程
面向接口编程是面向对象编程的一部分。
为什么需要面向接口编程? 软件设计中最难处理的就是需求的复杂变化,需求的变化更多的体现在具体实现上。我们的编程如果围绕具体实现来展开就会陷入”复杂变化”的汪洋大海中,软件也就不能最终实现。我们必须围绕某种稳定的东西开展,才能以静制动,实现规范的高质量的项目。
接口就是规范,就是项目中最稳定的东东! 面向接口编程可以让我们把握住真正核心的东西,使实现复杂多变的需求成为可能。
通过面向接口编程,而不是面向实现类编程,可以大大降低程序模块间的耦合性,提高整个系统的可扩展性和和可维护性。
面向接口编程的概念比接口本身的概念要大得多。设计阶段相对比较困难,在你没有写实现时就要想好接口,接口一变就乱套了,所以设计要比实现难!
老鸟建议
接口语法本身非常简单,但是如何真正使用?这才是大学问。我们需要后面在项目中反复使用,大家才能体会到。 学到此处,能了解基本概念,熟悉基本语法,就是“好学生”了。 请继续努力!再请工作后,闲余时间再看看上面这段话,相信你会有更深的体会。
5.10.1内部类的概念
一般情况,我们把类定义成独立的单元。有些情况下,我们把一个类放在另一个类的内部定义,称为内部类(innerclasses)。
内部类可以使用public、default、protected 、private以及static修饰。而外部顶级类(我们以前接触的类)只能使用public和default修饰。
注意
内部类只是一个编译时概念,一旦我们编译成功,就会成为完全不同的两个类。对于一个名为Outer的外部类和其内部定义的名为Inner的内部类。编译完成后会出现Outer.class和Outer$Inner.class两个类的字节码文件。所以内部类是相对独立的一种存在,其成员变量/方法名可以和外部类的相同。
【示例5-19】内部类介绍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 /*外部类Outer/
class Outer {
private int age = 10;
public void show(){
System.out.println(age);//10
}
/*内部类Inner/
public class Inner {
//内部类中可以声明与外部类同名的属性与方法
private int age = 20;
public void show(){
System.out.println(age);//20
}
}
}
示例5-19编译后会产生两个不同的字节码文件,如图5-24所示:
图5-24 内部类编译结果图
内部类的作用:
1. 内部类提供了更好的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问。
2. 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。 但外部类不能访问内部类的内部属性。
3. 接口只是解决了多重继承的部分问题,而内部类使得多重继承的解决方案变得更加完整。
内部类的使用场合:
1. 由于内部类提供了更好的封装特性,并且可以很方便的访问外部类的属性。所以,在只为外部类提供服务的情况下可以优先考虑使用内部类。
2. 使用内部类间接实现多继承:每个内部类都能独立地继承一个类或者实现某些接口,所以无论外部类是否已经继承了某个类或者实现了某些接口,对于内部类没有任何影响。
5.10.2内部类的分类
在Java中内部类主要分为成员内部类(非静态内部类、静态内部类)、匿名内部类、局部内部类。
.成员内部类(可以使用private、default、protected、public任意进行修饰。 类文件:外部类$内部类.class)
a) 非静态内部类(外部类里使用非静态内部类和平时使用其他类没什么不同)
i. 非静态内部类必须寄存在一个外部类对象里。因此,如果有一个非静态内部类对象那么一定存在对应的外部类对象。非静态内部类对象单独属于外部类的某个对象。
ii. 非静态内部类可以直接访问外部类的成员,但是外部类不能直接访问非静态内部类成员。
iii. 非静态内部类不能有静态方法、静态属性和静态初始化块。
iv. 外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例。
v. 成员变量访问要点:
1. 内部类里方法的局部变量:变量名。
2. 内部类属性:this.变量名。
3. 外部类属性:外部类名.this.变量名。
【示例5-20】成员变量的访问要点
1
2
3
4
5
6
7
8
9
10
11
12 class Outer {
private int age = 10;
class Inner {
int age = 20;
public void show() {
int age = 30;
System.out.println(“内部类方法里的局部变量age:” + age);// 30
System.out.println(“内部类的成员变量age:” + this.age);// 20
System.out.println(“外部类的成员变量age:” + Outer.this.age);// 10
}
}
}
vi. 内部类的访问:
1. 外部类中定义内部类:
1 new Inner()
2. 外部类以外的地方使用非静态内部类:
1 Outer.Inner varname = new Outer().new Inner()。
【示例5-21】内部类的访问
1
2
3
4
5
6
7
8
9
10 public class TestInnerClass {
public static void main(String[] args) {
//先创建外部类实例,然后使用该外部类实例创建内部类实例
Outer.Inner inner = new Outer().new Inner();
inner.show();
Outer outer = new Outer();
Outer.Inner inn = outer.new Inner();
inn.show();
}
}
执行结果如图5-25所示:
图5-25 示例5-21运行效果图
b) 静态内部类
i. 定义方式:
1
2
3 static class ClassName {
//类体
}
ii. 使用要点:
1. 当一个静态内部类对象存在,并不一定存在对应的外部类对象。 因此,静态内部类的实例方法不能直接访问外部类的实例方法。
2. 静态内部类看做外部类的一个静态成员。 因此,外部类的方法中可以通过:“静态内部类.名字”的方式访问静态内部类的静态成员,通过 new 静态内部类()访问静态内部类的实例。
【示例5-22】静态内部类的访问
1
2
3
4
5
6
7
8
9
10
11 class Outer{
//相当于外部类的一个静态成员
static class Inner{
}
}
public class TestStaticInnerClass {
public static void main(String[] args) {
//通过 new 外部类名.内部类名() 来创建内部类对象
Outer.Inner inner =new Outer.Inner();
}
}
. 匿名内部类
适合那种只需要使用一次的类。比如:键盘监听操作等等。
语法:
1
2
3 new 父类构造器(实参类表) \实现接口 () {
//匿名内部类类体!
}
【示例5-23】匿名内部类的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 this.addWindowListener(new WindowAdapter(){
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
}
);
this.addKeyListener(new KeyAdapter(){
@Override
public void keyPressed(KeyEvent e) {
myTank.keyPressed(e);
}
@Override
public void keyReleased(KeyEvent e) {
myTank.keyReleased(e);
}
}
);
注意
1. 匿名内部类没有访问修饰符。
2. 匿名内部类没有构造方法。因为它连名字都没有那又何来构造方法呢。
.局部内部类
还有一种内部类,它是定义在方法内部的,作用域只限于本方法,称为局部内部类。
局部内部类的的使用主要是用来解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类。局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法中被使用,出了该方法就会失效。
局部内部类在实际开发中应用很少。
【示例5-24】方法中的内部类
1
2
3
4
5
6
7
8
9
10
11
12
13
14 public class Test2 {
public void show() {
//作用域仅限于该方法
class Inner {
public void fun() {
System.out.println(“helloworld”);
}
}
new Inner().fun();
}
public static void main(String[] args) {
new Test2().show();
}
}
执行结果如图5-26所示:
图5-26 示例5-24运行效果图
5.11.1 String基础
1. String类又称作不可变字符序列。
2. String位于java.lang包中,Java程序默认导入java.lang包下的所有类。
3. Java字符串就是Unicode字符序列,例如字符串“Java”就是4个Unicode字符’J’、’a’、’v’、’a’组成的。
4. Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义的类String,每个用双引号括起来的字符串都是String类的一个实例。
【示例5-25】String类的实例
String e = “” ; // 空字符串
String greeting = " Hello World “;
5. Java允许使用符号”+"把两个字符串连接起来。
【示例5-26】字符串连接
String s1 = “Hello”;
String s2 = "World! “;
String s = s1 + s2; //HelloWorld!
n-符号”+“把两个字符串按给定的顺序连接在一起,并且是完全按照给定的形式。
n-当”+“运算符两侧的操作数中只要有一个是字符串(String)类型,系统会自动将另一个操作数转换为字符串然后再进行连接。
【示例5-27】”+“连接符
int age = 18;
String str = “age is” + age; //str赋值为"age is 18”
//这种特性通常被用在输出语句中:
System.out.println(“age is” + age);
5.11.2 String类和常量池
在Java的内存分析中,我们会经常听到关于“常量池”的描述,实际上常量池也分了以下三种:
【示例5-29】String类常用方法一
1
2
3
4
5
6
7
8
9
10
11
12
13
14 public class StringTest1 {
public static void main(String[] args) {
String s1 = “core Java”;
String s2 = “Core Java”;
System.out.println(s1.charAt(3));//提取下标为3的字符
System.out.println(s2.length());//字符串的长度
System.out.println(s1.equals(s2));//比较两个字符串是否相等
System.out.println(s1.equalsIgnoreCase(s2));//比较两个字符串(忽略大小写)
System.out.println(s1.indexOf(“Java”));//字符串s1中是否包含Java
System.out.println(s1.indexOf(“apple”));//字符串s1中是否包含apple
String s = s1.replace(’ ', ‘&’);//将s1中的空格替换成&
System.out.println(“result is :” + s);
}
}
执行结果如图5-31所示:
图5-31 示例5-29运行效果图
【示例5-30】String类常用方法二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 public class StringTest2 {
public static void main(String[] args) {
String s = “”;
String s1 = “How are you?”;
System.out.println(s1.startsWith(“How”));//是否以How开头
System.out.println(s1.endsWith(“you”));//是否以you结尾
s = s1.substring(4);//提取子字符串:从下标为4的开始到字符串结尾为止
System.out.println(s);
s = s1.substring(4, 7);//提取子字符串:下标[4, 7) 不包括7
System.out.println(s);
s = s1.toLowerCase();//转小写
System.out.println(s);
s = s1.toUpperCase();//转大写
System.out.println(s);
String s2 = " How old are you!! ";
s = s2.trim();//去除字符串首尾的空格。注意:中间的空格不能去除
System.out.println(s);
System.out.println(s2);//因为String是不可变字符串,所以s2不变
}
}
执行结果如图5-32所示:
图5-32 示例5-30运行效果图
5.11.5 字符串相等的判断
1. equals方法用来检测两个字符串内容是否相等。如果字符串s和t内容相等,则s.equals(t)返回true,否则返回false。
2. 要测试两个字符串除了大小写区别外是否是相等的,需要使用equalsIgnoreCase方法。
3. 判断字符串是否相等不要使用"“。
【示例5-31】忽略大小写的字符串比较
1 “Hello”.equalsIgnoreCase(“hellO”);//true
【示例5-32】字符串的比较”"与equals()方法
1
2
3
4
5
6
7
8
9
10 public class TestStringEquals {
public static void main(String[] args) {
String g1 = “北京尚学堂”;
String g2 = “北京尚学堂”;
String g3 = new String(“北京尚学堂”);
System.out.println(g1 == g2); // true 指向同样的字符串常量对象
System.out.println(g1 == g3); // false g3是新创建的对象
System.out.println(g1.equals(g3)); // true g1和g3里面的字符串内容是一样的
}
}
执行结果如图5-33所示:
图5-33 示例5-32运行效果图
示例5-32的内存分析如图5-34所示:
图5-34 示例5-32内存分析图
5.12.1 开闭原则
开闭原则(Open-Closed Principle)就是让设计的系统对扩展开放,对修改封闭。
· 对扩展开放:
就是指,应对需求变化要灵活。 要增加新功能时,不需要修改已有的代码,增加新代码即可。
· 对修改关闭:
就是指,核心部分经过精心设计后,不再因为需求变化而改变。
在实际开发中,我们无法完全做到,但应尽量遵守开闭原则。
5.12.2 模板方法模式和回调机制
模板方法模式很常用,其目的是在一个方法中定义一个算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。在标准的模板方法模式实现中,主要是使用继承的方式,来让父类在运行期间可以调用到子类的方法。 详见抽象类部分示例。
其实在Java开发中,还有另外一个方法可以实现同样的功能,那就是Java回调技术。回调是一种双向的调用模式,也就是说,被调用的接口被调用时也会调用对方的接口,简单点说明就是:A类中调用B类中的C方法,然后B类中的C方法中反过来调用A类中的D方法,那么D这个方法就叫回调方法。
回调的具体过程如下:
1. Class A实现接口CallBack —— 背景1
2. class A中包含class B的引用 ——背景2
3. class B有一个参数为CallBack的方法C ——背景3
4. 前三条是我们的准备条件,接下来A的对象调用B的方法C
5. 然后class B就可以在C方法中调用A的方法D
这样说大家可能还是不太理解,下面我们根据示例5-33来说明回调机制。该示例的生活背景为:有一天小刘遇到一个很难的问题“学习Java选哪家机构呢?”,于是就打电话问小高,小高一时也不太了解行情,就跟小刘说,我现在还有事,等忙完了给你咨询咨询,小刘也不会傻傻的拿着电话去等小高的答案,于是小刘对小高说,先挂电话吧,你知道答案后再打我电话告诉我吧,于是挂了电话。小高先去办自己的事情去了,过了几个小时,小高打电话给小刘,告诉他答案是“学Java当然去北京尚学堂”。
【示例5-33】回调机制示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69 /**
回调接口
/
interface CallBack {
/*
小刘类:实现了回调接口CallBack(背景一)
/
class Liu implements CallBack {
/*
public Liu(Gao gao){
this.gao = gao;
}
/**
小高类
/
class Gao {
/*
public class Test {
public static void main(String[] args) {
Gao gao= new Gao();
Liu liu = new Liu(gao);
//小刘问问题
liu.askQuestion(“学习Java选哪家机构呢?”);
}
}
执行结果如图5-34所示:
图5-34 示例5-33运行效果图
通过回调在接口中定义的方法,调用到具体的实现类中的方法,其本质是利用Java的动态绑定技术,在这种实现中,可以不把实现类写成单独的类,而使用内部类或匿名内部类来实现回调方法。
5.12.3 组合模式
组合模式是将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
【示例5-34】对象的组合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 class Cpu {
public void run() {
System.out.println(“quickly…”);
}
}
class MainBoard {
public void connect() {
System.out.println(“connect…”);
}
}
class Memory {
public void store() {
System.out.println(“store…”);
}
}
public class Computer {
Cpu cpu;
Memory memory;
MainBoard mainBoard;
public void work() {
cpu.run();
memory.store();
mainBoard.connect();
}
public static void main(String[] args) {
Computer computer = new Computer();
computer.cpu = new Cpu();
computer.mainBoard = new MainBoard();
computer.memory = new Memory();
computer.work();
}
}
执行结果如图5-36所示:
图5-36 示例5-34运行效果图
第五章 总结
· 高级语言可分为:面向过程和面向对象两大类
1. 面向过程与面向对象都是解决问题的思维方式,都是代码组织的方式。
2. 解决简单问题可以使用面向过程。
3. 解决复杂问题:宏观上使用面向对象把握,微观处理上仍然是面向过程。
· 对象和类的关系是特殊到一般,具体到抽象的关系。
· 栈内存
1. 每个线程私有,不能实现线程间的共享!
2. 局部变量放置于栈中。
3. 栈是由系统自动分配,速度快!栈是一个连续的内存空间!
· 堆内存
1. 放置new出来的对象!
2. 堆是一个不连续的内存空间,分配灵活,速度慢!
· 方法区
1. 被所有线程共享!
2. 用来存放程序中永远是不变或唯一的内容(类代码信息、静态变量、字符串常量)。
· 属性用于定义该类或该类对象包含的数据或者说静态属性。属性作用范围是整个类体。Java使用默认的值对其初始化。
· 方法则用于定义该类或该类实例的行为特征和功能实现。方法是类和对象行为特征的抽象。
· 构造器又叫做构造方法(constructor),用于构造该类的实例。Java通过new关键字来调用构造方法,从而返回该类的实例,是一种特殊的方法。
· 垃圾回收机制
1. 程序员无权调用垃圾回收器。
2. 程序员可以通过System.gc()通知垃圾回收器(Garbage Collection,简称GC)运行,但是Java规范并不能保证立刻运行。
3. finalize方法,是Java提供给程序员用来释放对象或资源的方法,但是尽量少用。
· 方法的重载是指一个类中可以定义有相同的名字,但参数不同的多个方法。 调用时,会根据不同的参数表选择对应的方法。
· this关键字的作用
1. 让类中的一个方法,访问该类的另一个方法或属性。
2. 使用this关键字调用重载构造方法,可以避免相同的初始化代码,只能在构造方法中用,并且必须位于构造方法的第一句。
· static关键字
1. 在类中,用static声明的成员变量为静态成员变量,也称为类变量。
2. 用static声明的方法为静态方法。
3. 可以通过对象引用或类名(不需要实例化)访问静态成员。
· package的作用
1. 可以解决类之间的重名问题。
2. 便于管理类:合适的类位于合适的包!
· impport的作用
1. 通过import可以导入其他包下面的类,从而可以在本类中直接通过类名来调用。
· super关键字的作用
1. super是直接父类对象的引用。可以通过super来访问父类中被子类覆盖的方法或属性。
· 面向对象的三大特征:继承、封装、多态。
· Object类是所有Java类的根基类。
· 访问权限控制符:范围由小到大分别是private、default、protected、public。
· 引用变量名 instanceof 类名 来判断该引用类型变量所“指向”的对象是否属于该类或该类的子类。
· final关键字可以修饰变量、修饰方法、修饰类。
· 抽象类是一种模版模式。抽象类为所有子类提供了一个通用模版,子类可以在这个模版基础上进行扩展,使用abstract修饰。
· 使用abstract修饰的方法为抽象方法必须被子类实现,除非子类也是抽象类。
· 使用interface声明接口
1. 从接口的实现者角度看,接口定义了可以向外部提供的服务。
2. 从接口的调用者角度看,接口定义了实现者能提供哪些服务。
· 内部类分为成员内部类、匿名内部类和局部内部类。
· String位于java.lang包中,Java程序默认导入java.lang包。
· 字符串的比较"=="与equals()方法的区别。
第五章 作业
一、选择题
1.使用权限修饰符( )修饰的类的成员变量和成员方法,可以被当前包中所有类访问,也可以被它的子类(同一个包以及不同包中的子类)访问。(选择一项) b
Apublic
B.protected
C.默认
D.private
2.以下关于继承条件下构造方法执行过程的代码的执行结果是( )。(选择一项) a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 class Person {
public Person() {
System.out.println(“execute Person()”);
}
}
class Student extends Person {
public Student() {
System.out.println("execute Student() ");
}
}
class PostGraduate extends Student {
public PostGraduate() {
System.out.println(“execute PostGraduate()”);
}
}
public class TestInherit {
public static void main(String[] args) {
new PostGraduate();
}
}
A.execute Person()
execute Student()
execute PostGraduate()
B.execute PostGraduate()
C.execute PostGraduate()
execute Student()
execute Person()
D.没有结果输出
3.编译运行如下Java代码,输出结果是( )。(选择一项)d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 class Base {
public void method(){
System.out.print (“Base method”);
}
}
class Child extends Base{
public void methodB(){
System.out.print (“Child methodB”);
}
}
class Sample {
public static void main(String[] args) {
Base base= new Child();
base.methodB();
}
}
A.Base method
B.Child methodB
C.hild methodB
D.编译错误
4.在Java中关于abstract关键字,以下说法正确的是( )。 B
A.abstract类中可以没有抽象方法
B.abstract类的子类也可以是抽象类
C.abstract方法可以有方法体
D.abstract类可以创建对象
5.在Java接口中,下列选项中属于有效的方法声明是( )。(选择二项) AC
A.public void aMethod( );
B.final void aMethod( );
C.void aMethod();
D.private void aMethod( );
二、简答题
1private、默认、protected、public四个权限修饰符的作用。
2、继承条件下子类构造方法的执行过程。
构造方法第一句总是:super(…)来调用父类对应的构造方法。所以,流程就是:先向上追溯到Object,然后再依次向下执行类的初始化块和构造方法,直到当前子类为止。
3、什么是向上转型和向下转型。
向上转型即为父类引用指向子类对象,向下转型为类似为强制转型,把引用变量转成真实的子类类型(运行时类型)
4、final和abstract关键字的作用。
final作用于类不可被继承,作用于方法不可重写但可重载,作用于变量不可更改。
5、==和equals()的联系和区别。
==作用基本类型代表值相等,作用引用类型代表地址相等。
equals()通过比较hashcode来比较是否为同一个对象,可重写。
三、编码题
1、编写应用程序,创建类的对象,分别设置圆的半径、圆柱体的高,计算并分别显示圆半径、圆面积、圆周长,圆柱体的体积。
实现思路及关键代码:
编写一个圆类Circle,该类拥有:
a) 一个成员变量,radius(私有,浮点型);//存放圆的半径
b) 两个构造方法(无参、有参);
c) 三个成员方法
double getArea() //获取圆的面积
double getPerimeter() //获取圆的周长
void show() //将圆的关径、周长、面积输出到屏幕
编写一个圆柱体类Cylinder,它继承于上面的Circle类。还拥有:
a) 一个成员变量,double hight (私有,浮点型); //圆柱体的高;
b) 构造方法
c) 成员方法
double getVolume() //获取圆柱体的体积
void showVolume() //将圆柱体的体积输出到屏幕
class Circle{
private float radius;
public Circle() {
radius = 2; //默认值-半径
}
public Circle(float radius) {
this();
this.radius = radius;
}
double getArea() {
return 3.14*this.radius*this.radius;
}
double getPerimeter() {
return 2*3.14*this.radius;
}
void show() {
System.out.println("圆的半径为:"+this.radius+",圆的周长为:"+getPerimeter()+",圆的面积为:"+getArea());
}
}
class Cylinder extends Circle{
private float height;
public Cylinder() { super(); height = 3;//默认值-高 } public Cylinder(float radius ,float height) { super(radius); this.height = height; } double getVolume() { return super.getArea()*this.height; } void showVolume() { System.out.println("圆柱体的体积为:"+super.getArea()*this.height); }
}
public class Fifth1 {
public static void main(String[] args) {
Circle circle = new Circle(6);
circle.show();
Cylinder cylinder = new Cylinder(6,6);
cylinder.show();
cylinder.showVolume();
}
}
2、编写程序实现乐手弹奏乐器。乐手可以弹奏不同的乐器从而发出不同的声音。可以弹奏的乐器包括二胡、钢琴和琵琶。
实现思路及关键代码:
定义乐器类Instrument,包括方法makeSound();
定义乐器类的子类:二胡Erhu、钢琴Piano和小提琴Violin;
定义乐手类Musician,可以弹奏各种乐器play(Instrument i);
定义测试类,给乐手不同的乐器让他弹奏。
class Instrument{
public void makesound() {
System.out.println(“发出声音”);
}
}
class Erhu extends Instrument{
@Override
public void makesound() {
System.out.println(“二泉映月”);
}
}
class Piano extends Instrument{
@Override
public void makesound() {
System.out.println(“蓝色多瑙河”);
}
}
class Violin extends Instrument{
@Override
public void makesound() {
System.out.println(“门德尔松E小调协奏曲”);
}
}
class Musician{
public void play(Instrument i) {
i.makesound();
}
}
public class Fifth2 {
public static void main(String[] args) {
Instrument erhu = new Erhu();
Instrument piano = new Piano();
Instrument Violin = new Violin();
Musician a = new Musician();
a.play(erhu);
a.play(piano);
a.play(Violin);
}
}
3、编写程序描述影视歌三栖艺人。需求说明:请使用面向对象的思想,设计自定义类,描述影视歌三梄艺人。
实现思路:
分析影视歌三栖艺人的特性:可以演电影,可以演电视剧,可以唱歌
定义多个接口描述特性
a) 演电影的接口-----方法:演电影
b) 演电视剧的接口-----方法:演电视剧
c) 唱歌的接口-----方法:唱歌
定义艺人类实现多个接口
程序运行结果如图5-36所示。
图5-37 编码题3运行效果图
interface Movie{
void actMovie();
}
interface TvSeries{
void actTvSeries();
}
interface Sing{
void singing();
}
class Actor implements Movie,TvSeries,Sing{
private String name;
public Actor(String name) { this.name = name; } @Override public void actMovie() { System.out.println("我能演电影"); } @Override public void actTvSeries() { System.out.println("我能演电视剧"); } @Override public void singing() { System.out.println("我会唱歌"); } public void intr() { System.out.println("大家好,我叫"+name); actMovie(); actTvSeries(); singing(); }
}
public class Fifth3 {
public static void main(String[] args) {
Actor a = new Actor(“杨幂”);
a.intr();
}
}
第六章 异常机制
6.1 导引问题
【专业IT培训机构,真正零首付入学www.bjsxt.com】
在实际工作中,我们遇到的情况不可能是非常完美的。比如:你写的某个模块,用户输入不一定符合你的要求;你的程序要打开某个文件,这个文件可能不存在或者文件格式不对;你要读取数据库的数据,数据可能是空的;我们的程序再运行着,但是内存或硬盘可能满了等等。
软件程序在运行过程中,非常可能遇到刚刚提到的这些问题,我们称之为异常,英文是:Exception,意思是例外。遇到这些例外情况,或者叫异常,我们怎么让写的程序做出合理的处理,安全的退出,而不至于程序崩溃呢?我们本章就要讲解这些问题。
如果我们要拷贝一个文件,在没有异常机制的情况下,我们需要考虑各种异常情况,伪代码如下:
【示例6-1】伪代码使用if处理程序中可能出现的各种情况
public class Test1 {
public static void main(String[] args) {
//将d:/a.txt复制到e:/a.txt
if("d:/a.txt"这个文件存在){
if(e盘的空间大于a.txt文件长度){
if(文件复制一半IO流断掉){
停止copy,输出:IO流出问题!
}else{
copyFile(“d:/a.txt”,“e:/a.txt”);
}
}else{
System.out.println(“e盘空间不够存放a.txt!”);
}
}else{
System.out.println(“a.txt不存在!”);
}
}
}
这种方式,有两个坏处:
1. 逻辑代码和错误处理代码放一起!
2. 程序员本身需要考虑的例外情况较复杂,对程序员本身要求较高!
那么,我们如何解决应对异常情况呢?Java的异常机制给我们提供了方便的处理方式。如上情况,如果是用Java的异常机制来处理,示意代码如下(仅限示意,不能运行):
try {
copyFile(“d:/a.txt”,“e:/a.txt”);
} catch (Exception e) {
e.printStackTrace();
}
异常机制本质
就是当程序出现错误,程序安全退出的机制。
6.2 异常(Exception)的概念
异常指程序运行过程中出现的非正常现象,例如用户输入错误、除数为零、需要处理的文件不存在、数组下标越界等。
在Java的异常处理机制中,引进了很多用来描述和处理异常的类,称为异常类。异常类定义中包含了该类异常的信息和对异常进行处理的方法。
所谓异常处理,就是指程序在出现问题时依然可以正确的执行完。
我们开始看我们的第一个异常对象,并分析一下异常机制是如何工作的。
【示例6-2】异常的分析
1
2
3
4
5
6 public class Test2 {
public static void main(String[] args) {
int i=1/0; //除数为0
System.out.println(i);
}
}
执行结果如图6-1所示:
图6-1 示例6-2运行效果图
Java是采用面向对象的方式来处理异常的。处理过程:
1. 抛出异常:在执行一个方法时,如果发生异常,则这个方法生成代表该异常的一个对象,停止当前执行路径,并把异常对象提交给JRE。
2. 捕获异常:JRE得到该异常后,寻找相应的代码来处理该异常。JRE在方法的调用栈中查找,从生成异常的方法开始回溯,直到找到相应的异常处理代码为止。
6.3 异常分类
JDK 中定义了很多异常类,这些类对应了各种各样可能出现的异常事件,所有异常对象都是派生于Throwable类的一个实例。如果内置的异常类不能够满足需要,还可以创建自己的异常类。
Java对异常进行了分类,不同类型的异常分别用不同的Java类表示,所有异常的根类为java.lang.Throwable,Throwable下面又派生了两个子类:Error和Exception。Java异常类的层次结构如图6-2所示。
图6-2 Java异常类层次结构图
6.3.1 Error
Error是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
Error表明系统JVM已经处于不可恢复的崩溃状态中。我们不需要管它。
图6-3 java.lang包中Error的类
Error与Exception的区别
1. 我开着车走在路上,一头猪冲在路中间,我刹车。这叫一个异常。
2. 我开着车在路上,发动机坏了,我停车,这叫错误。系统处于不可恢复的崩溃状态。发动机什么时候坏?我们普通司机能管吗?不能。发动机什么时候坏是汽车厂发动机制造商的事
6.3.2 Exception
Exception是程序本身能够处理的异常,如:空指针异常(NullPointerException)、数组下标越界异常(ArrayIndexOutOfBoundsException)、类型转换异常(ClassCastException)、算术异常(ArithmeticException)等。
Exception类是所有异常类的父类,其子类对应了各种各样可能出现的异常事件。 通常Java的异常可分为:
1. RuntimeException 运行时异常
2. CheckedException 已检查异常
6.3.3 RuntimeException运行时异常
派生于RuntimeException的异常,如被 0 除、数组下标越界、空指针等,其产生比较频繁,处理麻烦,如果显式的声明或捕获将会对程序可读性和运行效率影响很大。 因此由系统自动检测并将它们交给缺省的异常处理程序(用户可不必对其处理)。
这类异常通常是由编程错误导致的,所以在编写程序时,并不要求必须使用异常处理机制来处理这类异常,经常需要通过增加“逻辑处理来避免这些异常”。
【示例6-3】ArithmeticException异常:试图除以0
1
2
3
4
5
6 public class Test3 {
public static void main(String[] args) {
int b=0;
System.out.println(1/b);
}
}
执行结果如图6-2所示:
图6-4 ArithmeticException异常
解决如上异常需要修改代码:
1
2
3
4
5
6
7
8 public class Test3 {
public static void main(String[] args) {
int b=0;
if(b!=0){
System.out.println(1/b);
}
}
}
当程序访问一个空对象的成员变量或方法,或者访问一个空数组的成员时会发生空指针异常(NullPointerException)。怎么处理?
【示例6-4】NullPointerException异常
1
2
3
4
5
6 public class Test4 {
public static void main(String[] args) {
String str=null;
System.out.println(str.charAt(0));
}
}
执行结果如图6-5所示:
图6-5 NullPointerException异常
解决空指针异常,通常是增加非空判断:
1
2
3
4
5
6
7
8 public class Test4 {
public static void main(String[] args) {
String str=null;
if(str!=null){
System.out.println(str.charAt(0));
}
}
}
在引用数据类型转换时,有可能发生类型转换异常(ClassCastException)。
【示例6-5】ClassCastException异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 class Animal{
}
class Dog extends Animal{
}
class Cat extends Animal{
}
public class Test5 {
public static void main(String[] args) {
Animal a=new Dog();
Cat c=(Cat)a;
}
}
执行结果如图6-6所示:
图6-6 ClassCastException异常
解决ClassCastException的典型方式:
1
2
3
4
5
6
7
8 public class Test5 {
public static void main(String[] args) {
Animal a = new Dog();
if (a instanceof Cat) {
Cat c = (Cat) a;
}
}
}
当程序访问一个数组的某个元素时,如果这个元素的索引超出了0~数组长度-1这个范围,则会出现数组下标越界异常(ArrayIndexOutOfBoundsException)。
【示例6-6】ArrayIndexOutOfBoundsException异常
1
2
3
4
5
6 public class Test6 {
public static void main(String[] args) {
int[] arr = new int[5];
System.out.println(arr[5]);
}
}
执行结果如图6-7所示:
图6-7 ArrayIndexOutOfBoundsException异常
解决数组索引越界异常的方式,增加关于边界的判断:
1
2
3
4
5
6
7
8
9 public class Test6 {
public static void main(String[] args) {
int[] arr = new int[5];
int a = 5;
if (a < arr.length) {
System.out.println(arr[a]);
}
}
}
在使用包装类将字符串转换成基本数据类型时,如果字符串的格式不正确,则会出现数字格式异常(NumberFormatException)。
【示例6-7】NumberFormatException异常
1
2
3
4
5
6 public class Test7 {
public static void main(String[] args) {
String str = “1234abcf”;
System.out.println(Integer.parseInt(str));
}
}
执行结果如图6-8所示:
图6-8 NumberFormatException异常
数字格式化异常的解决,可以引入正则表达式判断是否为数字:
1
2
3
4
5
6
7
8
9
10
11
12
13 import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Test7 {
public static void main(String[] args) {
String str = “1234abcf”;
Pattern p = Pattern.compile(“^\d+$”);
Matcher m = p.matcher(str);
if (m.matches()) { // 如果str匹配代表数字的正则表达式,才会转换
System.out.println(Integer.parseInt(str));
}
}
}
注意事项
1. 在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。
2. 运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。
6.3.4 CheckedException已检查异常
所有不是RuntimeException的异常,统称为Checked Exception,又被称为“已检查异常”,如IOException、SQLException等以及用户自定义的Exception异常。 这类异常在编译时就必须做出处理,否则无法通过编译。如图6-9所示。
图6-9 CheckedException异常
如图6-9所示,异常的处理方式有两种:使用“try/catch”捕获异常、使用“throws”声明异常。
6.4 异常的处理方式之一:捕获异常
捕获异常是通过3个关键词来实现的:try-catch-finally。用try来执行一段程序,如果出现异常,系统抛出一个异常,可以通过它的类型来捕捉(catch)并处理它,最后一步是通过finally语句为异常处理提供一个统一的出口,finally所指定的代码都要被执行(catch语句可有多条;finally语句最多只能有一条,根据自己的需要可有可无)。如图6-10所示。
图6-10 异常处理
上面过程详细解析:
public class Test9 {
public static void main(String[] args) {
try {
readFile(“joke.txt”);
} catch (FileNotFoundException e) {
System.out.println(“所需文件不存在!”);
} catch (IOException e) {
System.out.println(“文件读写错误!”);
}
}
public static void readFile(String fileName) throws FileNotFoundException,
IOException {
FileReader in = new FileReader(fileName);
int tem = 0;
try {
tem = in.read();
while (tem != -1) {
System.out.print((char) tem);
tem = in.read();
}
} finally {
in.close();
}
}
}
注意事项
1. 方法重写中声明异常原则:子类重写父类方法时,如果父类方法有声明异常,那么子类声明的异常范围不能超过父类声明的范围。
6.6 自定义异常
1.在程序中,可能会遇到JDK提供的任何标准异常类都无法充分描述清楚我们想要表达的问题,这种情况下可以创建自己的异常类,即自定义异常类。
2.自定义异常类只需从Exception类或者它的子类派生一个子类即可。
3.自定义异常类如果继承Exception类,则为受检查异常,必须对其进行处理;如果不想处理,可以让自定义异常类继承运行时异常RuntimeException类。
4.习惯上,自定义异常类应该包含2个构造器:一个是默认的构造器,另一个是带有详细信息的构造器。
【示例6-10】自定义异常类
1
2
3
4
5
6
7
8
9
10
11 /*IllegalAgeException:非法年龄异常,继承Exception类/
class IllegalAgeException extends Exception {
//默认构造器
public IllegalAgeException() {
}
//带有详细信息的构造器,信息存储在message中
public IllegalAgeException(String message) {
super(message);
}
}
【示例6-11】自定义异常类的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 class Person {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) throws IllegalAgeException {
if (age < 0) {
throw new IllegalAgeException("人的年龄不应该为负数");
}
this.age = age;
}
public String toString() {
return "name is " + name + " and age is " + age;
}
}
public class TestMyException {
public static void main(String[] args) {
Person p = new Person();
try {
p.setName(“Lincoln”);
p.setAge(-1);
} catch (IllegalAgeException e) {
e.printStackTrace();
System.exit(-1);
}
System.out.println§;
}
}
执行结果如图6-11所示:
图6-11 示例6-11运行效果图
使用异常机制的建议
1.要避免使用异常处理代替错误处理,这样会降低程序的清晰性,并且效率低下。
2.处理异常不可以代替简单测试—只在异常情况下使用异常机制。
3.不要进行小粒度的异常处理—应该将整个任务包装在一个try语句块中。
4.异常往往在高层处理(先了解!后面做项目会说!) 。
6.7 如何利用百度解决异常问题
正常学习和开发中,我们经常会遇到各种异常。大家在遇到异常时,需要遵循下面四步来解决:
1. 细心查看异常信息,确定异常种类和相关Java代码行号;
2. 拷贝异常信息到百度,查看相关帖子,寻找解决思路;
3. 前两步无法搞定,再问同学或同事;
4. 前三步无法搞定,请示领导。
很多同学碰到异常一下就慌了,立刻开始请教别人搬救兵,殊不知这样做有两大坏处。第一、太不尊重别人,把别人当苦力。第二、失去提高自我的机会,自己解决一个异常,就意味着有能力解决一类异常。解决一类异常能大大提高自身能力。
第六章 总结
Error与Exception都继承自Throwable类
Error类层次描述了Java运行时系统内部错误和资源耗尽错误。
Exception类是所有异常类的父类,其子类对应了各种各样可能出现的异常事件。
常见的异常类型
–ArithmeticException
–NullPointerException
–ClassCastException
–ArrayIndexOutOfBoundsException
–NumberFormatException
方法重写中声明异常原则:子类声明的异常范围不能超过父类声明的范围
异常处理的三种方式
–捕获异常:try-catch-finally
–声明异常:throws
自定义异常类只需从Exception类或者它的子类派生一个子类即可。
第六章 作业
一、选择题
1.以下关于异常的代码的执行结果是( )。(选择一项) C
1
2
3
4
5
6
7
8
9
10
11
12 public class Test {
public static void main(String args[]) {
try {
System.out.print(“try”);
return;
} catch(Exception e){
System.out.print(“catch”);
}finally {
System.out.print(“finally”);
}
}
}
A.try catch finally
B.catch finally
C.try finally
D.try
2.在异常处理中,如释放资源、关闭文件等由( )来完成。(选择一项) C
Atry子句
B.catch子句
C.finally子句
D.throw子句
3.阅读如下Java代码,其中错误的行是( )。(选择二项) AC
1
2
3
4
5
6
7
8
9
10 public class Student {
private String stuId;
public void setStuId(String stuId) throw Exception { // 1
if (stuId.length() != 4) { // 2
throws new Exception(“学号必须为4位!”); // 3
} else {
this.stuId = stuId; //4
}
}
}
A.1
B.2
C.3
D.全部正确
4.下面选项中属于运行时异常的是( )。(选择二项) CD
A.Exception和SexException
B.NullPointerException和InputMismatchException
C.ArithmeticException和ArrayIndexOutOfBoundsException
D.ClassNotFoundException和ClassCastException
5.阅读如下Java代码,在控制台输入"-1",执行结果是()。(选择一项) B
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 public class Demo {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print(“请输入数字:”);
try {
int num = input.nextInt();
if (num < 1 || num > 4) {
throw new Exception(“必须在1-4之间!”);
}
} catch (InputMismatchException e) {
System.out.println(“InputMismatchException”);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
A.输出:InputMismatchException
B.输出:必须在1-4之间!
C.什么也没输出
D.编译错误
二、简答题
Error和Exception的区别。
Error是程序无法处理的错误,表明系统JVM已经处于不可恢复的崩溃状态中。
Exception是程序本身能够处理的异常。
2.Checked异常和Runtime异常的区别。
Runtime异常包含了一大类被 0 除、数组下标越界、空指针等异常,产生频繁,如果用throw或try catch对可读性和运行效率影响较大,从而通过系统检测和处理的异常。
Checked异常为除了Runtime异常后剩下的异常,需要在编译时就进行处理,否则无法通过编译。
Java异常处理中,关键字try、catch、finally、throw、throws分别代表什么含义?
try指定了异常捕获并处理的范围。
catch代表异常捕获,捕获异常对象并作相应处理。
throw用于手动地抛出异常对象,throw后面需要一个异常对象。
throws用于在方法签名中声明抛出一个或多个异常类,throws关键字后可以紧跟一个或多个异常类。
throws和throw的区别。
throw用于手动地抛出异常对象,throw后面需要一个异常对象。
throws用于在方法签名中声明抛出一个或多个异常类,throws关键字后可以紧跟一个或多个异常类。
1编写程序接收用户输入分数信息,如果分数在0—100之间,输出成绩。如果成绩不在该范围内,抛出异常信息,提示分数必须在0—100之间。
要求:使用自定义异常实现。
import java.util.Scanner;
class IlegalException extends Exception{
public IlegalException() {
}
public IlegalException(String msg) {
super(msg);
}
}
class Score{
private float score;
public void setScore() throws IlegalException{
float getScore;
System.out.println("请输入分数:");
Scanner scanner = new Scanner(System.in);
getScore = scanner.nextFloat();
if(getScore<0||getScore>100) {
throw new IlegalException("分数不在0~100之间");
}
this.score = getScore;
}
}
public class Sixth1 {
public static void main(String[] args) throws IlegalException {
Score score = new Score();
score.setScore();
}
}
2\写一个方法void isTriangle(int a,int b,int c),判断三个参数是否能构成一个三角形, 如果不能则抛出异常IllegalArgumentException,显示异常信息 “a,b,c不能构成三角形”,如果可以构成则显示三角形三个边长,在主方法中得到命令行输入的三个整数, 调用此方法,并捕获异常。
import java.util.Scanner;
class IllegalArgumentException extends Exception{
public IllegalArgumentException() {
}
public IllegalArgumentException(String msg) {
super(msg);
}
}
public class Sixth2 {
static void isTriangle(int a,int b,int c) throws IllegalArgumentException { if(a+b<=c || a+c<=b || b+c<=a) { throw new IllegalArgumentException("a,b,c不能构成三角形"); } System.out.println("a="+a+",b="+b+",c="+c); } public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int a,b,c; System.out.println("请输入三角形的边长a:"); a=scanner.nextInt(); System.out.println("请输入三角形的边长b:"); b=scanner.nextInt(); System.out.println("请输入三角形的边长c:"); c=scanner.nextInt(); try { isTriangle(a, b, c); } catch (IllegalArgumentException e) { e.printStackTrace(); System.exit(-1); } }
}
3\编写一个计算N个学生分数平均分的程序。程序应该提示用户输入N的值,如何必须输入所有N个学生分数。如果用户输入的分数是一个负数,则应该抛出一个异常并捕获,提示“分数必须是正数或者0”。并提示用户再次输入该分数。
import java.util.Scanner;
class IlegalScoreException extends Exception{
public IlegalScoreException() {
}
public IlegalScoreException(String msg) {
super(msg);
}
}
public class Sixth3 {
public static void check(int k) throws IlegalScoreException{ if(k<0) { throw new IlegalScoreException("分数必须是正数或者0"); } } public static void main(String[] args) { int N; Scanner scanner = new Scanner(System.in); System.out.println("请输入学生数:"); N = scanner.nextInt(); for(int i =1;i<=N;i++) { int k; System.out.println("第"+i+"位学生成绩是:"); k=scanner.nextInt(); try { check(k); } catch (Exception e) { e.printStackTrace(); System.out.println("格式不正确,请重新输入"); i -= 1; } } }
}
第七章 数组
7.1数组概述和特点
数组的定义
数组是相同类型数据的有序集合。数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成。其中,每一个数据称作一个元素,每个元素可以通过一个索引(下标)来访问它们。数组的三个基本特点:
1. 长度是确定的。数组一旦被创建,它的大小就是不可以改变的。
2. 其元素必须是相同类型,不允许出现混合类型。
3. 数组类型可以是任何数据类型,包括基本类型和引用类型。
老鸟建议
数组变量属引用类型,数组也可以看成是对象,数组中的每个元素相当于该对象的成员变量。数组本身就是对象,Java中对象是在堆中的,因此数组无论保存原始类型还是其他对象类型,数组对象本身是在堆中存储的。
7.2.1 数组声明
【示例7-1】数组的声明方式有两种(以一维数组为例)
type[] arr_name; //(推荐使用这种方式)
type arr_name[];
注意事项
1. 声明的时候并没有实例化任何对象,只有在实例化数组对象时,JVM才分配空间,这时才与长度有关。
2. 声明一个数组的时候并没有数组真正被创建。
3. 构造一个数组,必须指定长度。
【示例7-2】创建基本类型一维数组
public class Test {
public static void main(String args[]) {
int[] s = null; // 声明数组;
s = new int[10]; // 给数组分配空间;
for (int i = 0; i < 10; i++) {
s[i] = 2 * i + 1;//给数组元素赋值;
System.out.println(s[i]);
}
}
}
图7-1 基本类型数组内存分配图
【示例7-3】创建引用类型一维数组
class Man{
private int age;
private int id;
public Man(int id,int age) {
super();
this.age = age;
this.id = id;
}
}
public class AppMain {
public static void main(String[] args) {
Man[] mans; //声明引用类型数组;
mans = new Man[10]; //给引用类型数组分配空间;
Man m1 = new Man(1,11);
Man m2 = new Man(2,22);
mans[0]=m1;//给引用类型数组元素赋值;
mans[1]=m2;//给引用类型数组元素赋值;
}
}
图7-2 引用类型数组内存分配图
7.2.2初始化
数组的初始化方式总共有三种:静态初始化、动态初始化、默认初始化。下面针对这三种方式分别讲解。
图7-3 示例7-7运行效果图
7.3.1 数组的遍历
数组元素下标的合法区间:[0, length-1]。我们可以通过下标来遍历数组中的元素,遍历时可以读取元素的值或者修改元素的值。
【示例7-7】 使用循环遍历初始化和读取数组
1
2
3
4
5
6
7
8
9
10
11
12
13 public class Test {
public static void main(String[] args) {
int[] a = new int[4];
//初始化数组元素的值
for(int i=0;i<a.length;i++){
a[i] = 100*i;
}
//读取元素的值
for(int i=0;i<a.length;i++){
System.out.println(a[i]);
}
}
}
执行结果如图7-3所示:
图7-3 示例7-7运行效果图
7.3.2 for-each循环
增强for循环for-each是JDK1.5新增加的功能,专门用于读取数组或集合中所有的元素,即对数组进行遍历。
【示例7-8】增强for循环
public class Test {
public static void main(String[] args) {
String[] ss = { “aa”, “bbb”, “ccc”, “ddd” };
for (String temp : ss) {
System.out.println(temp);
}
}
}
执行结果如图7-4所示:
图7-4 示例7-8运行效果图
注意事项
1. for-each增强for循环在遍历数组过程中不能修改数组中某元素的值。
2. for-each仅适用于遍历,不涉及有关索引(下标)的操作。
7.3.3数组的拷贝
System类里也包含了一个static void arraycopy(object src,int srcpos,object dest, int destpos,int length)方法,该方法可以将src数组里的元素值赋给dest数组的元素,其中srcpos指定从src数组的第几个元素开始赋值,length参数指定将src数组的多少个元素赋给dest数组的元素。
【示例7-9】数组拷贝
1
2
3
4
5
6
7
8
9
10 public class Test {
public static void main(String args[]) {
String[] s = {“阿里”,“尚学堂”,“京东”,“搜狐”,“网易”};
String[] sBak = new String[6];
System.arraycopy(s,0,sBak,0,s.length);
for (int i = 0; i < sBak.length; i++) {
System.out.print(sBak[i]+ “\t”);
}
}
}
执行结果如图7-5所示:
图7-5 示例7-9运行效果图
7.3.4 java.util.Arrays类
JDK提供的java.util.Arrays类,包含了常用的数组操作,方便我们日常开发。Arrays类包含了:排序、查找、填充、打印内容等常见的操作。
【示例7-10】打印数组
1
2
3
4
5
6
7
8 import java.util.Arrays;
public class Test {
public static void main(String args[]) {
int[] a = { 1, 2 };
System.out.println(a); // 打印数组引用的值;
System.out.println(Arrays.toString(a)); // 打印数组元素的值;
}
}
执行结果如图7-6所示:
图7-6 示例7-10运行效果图
菜鸟雷区
此处的Arrays.toString()方法是Arrays类的静态方法,不是前面讲的Object的toString()方法。
【示例7-11】数组元素的排序
1
2
3
4
5
6
7
8
9 import java.util.Arrays;
public class Test {
public static void main(String args[]) {
int[] a = {1,2,323,23,543,12,59};
System.out.println(Arrays.toString(a));
Arrays.sort(a);
System.out.println(Arrays.toString(a));
}
}
执行结果如图7-7所示:
图7-7 示例7-11运行效果图
【示例7-12】数组元素是引用类型的排序(Comparable接口的应用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 import java.util.Arrays;
public class Test {
public static void main(String[] args) {
Man[] msMans = { new Man(3, “a”), new Man(60, “b”), new Man(2, “c”) };
Arrays.sort(msMans);
System.out.println(Arrays.toString(msMans));
}
}
class Man implements Comparable {
int age;
int id;
String name;
public Man(int age, String name) { super(); this.age = age; this.name = name; } public String toString() { return this.name; } public int compareTo(Object o) { Man man = (Man) o; if (this.age < man.age) { return -1; } if (this.age > man.age) { return 1; } return 0; }
}
【示例7-13】二分法查找
1
2
3
4
5
6
7
8
9
10
11 import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] a = {1,2,323,23,543,12,59};
System.out.println(Arrays.toString(a));
Arrays.sort(a); //使用二分法查找,必须先对数组进行排序;
System.out.println(Arrays.toString(a));
//返回排序后新的索引位置,若未找到返回负数。
System.out.println(“该元素的索引:”+Arrays.binarySearch(a, 12));
}
}
执行结果如图7-8所示:
图7-8 示例7-13运行效果图
【示例7-14】数组填充
1
2
3
4
5
6
7
8
9 import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] a= {1,2,323,23,543,12,59};
System.out.println(Arrays.toString(a));
Arrays.fill(a, 2, 4, 100); //将2到4索引的元素替换为100;
System.out.println(Arrays.toString(a));
}
}
执行结果如图7-9所示:
图7-9 示例7-14运行效果图
7.4多维数组
多维数组可以看成以数组为元素的数组。可以有二维、三维、甚至更多维数组,但是实际开发中用的非常少。最多到二维数组(学习容器后,我们一般使用容器,二维数组用的都很少)。
【示例7-15】二维数组的声明
1
2
3
4
5
6
7
8
9
10 public class Test {
public static void main(String[] args) {
// Java中多维数组的声明和初始化应按从低维到高维的顺序进行
int[][] a = new int[3][];
a[0] = new int[2];
a[1] = new int[4];
a[2] = new int[3];
// int a1[][]=new int[][4];//非法
}
}
【示例7-16】二维数组的静态初始化
1
2
3
4
5
6 public class Test {
public static void main(String[] args) {
int[][] a = { { 1, 2, 3 }, { 3, 4 }, { 3, 5, 6, 7 } };
System.out.println(a[2][3]);
}
}
图7-10 示例7-16内存分配图
【示例7-17】二维数组的动态初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14 import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[][] a = new int[3][];
// a[0] = {1,2,5}; //错误,没有声明类型就初始化
a[0] = new int[] { 1, 2 };
a[1] = new int[] { 2, 2 };
a[2] = new int[] { 2, 2, 3, 4 };
System.out.println(a[2][3]);
System.out.println(Arrays.toString(a[0]));
System.out.println(Arrays.toString(a[1]));
System.out.println(Arrays.toString(a[2]));
}
}
执行结果如图7-11所示:
图7-11 示例7-17运行效果图
【示例7-18】获取数组长度
1
2
3
4 //获取的二维数组第一维数组的长度。
System.out.println(a.length);
//获取第二维第一个数组长度。
System.out.println(a[0].length);
7.5 数组存储表格数据
表格数据模型是计算机世界最普遍的模型,可以这么说,大家在互联网上看到的所有数据本质上都是“表格”,无非是表格之间互相套用。如下表格是一张雇员表:
表7-1 雇员表
我们观察表格,发现每一行可以使用一个一维数组存储:
Object[] a1 = {1001,“高淇”,18,“讲师”,“2006-2-14”};
Object[] a2 = {1002,“高小七”,19,“助教”,“2007-10-10”};
Object[] a3 = {1003,“高小琴”,20,“班主任”,“2008-5-5”};
注意事项
此处基本数据类型”1001”,本质不是Object对象。JAVA编译器会自动把基本数据类型“自动装箱”成包装类对象。大家在下一章学了包装类后就懂了。
这样我们只需要再定义一个二维数组,将上面3个数组放入即可:
Object[][] emps = new Object[3][];
emps[0] = a1;
emps[1] = a2;
emps[2] = a3;
【示例7-19】 二维数组保存表格数据
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
Object[] a1 = {1001,“高淇”,18,“讲师”,“2006-2-14”};
Object[] a2 = {1002,“高小七”,19,“助教”,“2007-10-10”};
Object[] a3 = {1003,“高小琴”,20,“班主任”,“2008-5-5”};
Object[][] emps = new Object[3][];
emps[0] = a1;
emps[1] = a2;
emps[2] = a3;
System.out.println(Arrays.toString(emps[0]));
System.out.println(Arrays.toString(emps[1]));
System.out.println(Arrays.toString(emps[2]));
}
}
执行结果如图7-12所示:
7.6.1 冒泡排序的基础算法
冒泡排序是最常用的排序算法,在笔试中也非常常见,能手写出冒泡排序算法可以说是基本的素养。
算法重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来,这样越大的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序算法的运作如下:
1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
3. 针对所有的元素重复以上的步骤,除了最后一个。
4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
大家可以用如上思想,将下面的人按照身高从低到高重新排列:
图7-13 身高图
【示例7-20】冒泡排序的基础算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] values = { 3, 1, 6, 2, 9, 0, 7, 4, 5, 8 };
bubbleSort(values);
System.out.println(Arrays.toString(values));
}
public static void bubbleSort(int[] values) {
int temp;
for (int i = 0; i < values.length; i++) {
for (int j = 0; j < values.length - 1 - i; j++) {
if (values[j] > values[j + 1]) {
temp = values[j];
values[j] = values[j + 1];
values[j + 1] = temp;
}
}
}
}
}
执行结果如图7-14所示:
图7-14 示例7-20运行效果图
7.6.2 冒泡排序的优化算法
其实,我们可以把7.6.1的冒泡排序的算法优化一下,基于冒泡排序的以下特点:
1.整个数列分成两部分:前面是无序数列,后面是有序数列。
2.初始状态下,整个数列都是无序的,有序数列是空。
3.每一趟循环可以让无序数列中最大数排到最后,(也就是说有序数列的元素个数增加1),也就是不用再去顾及有序序列。
4.每一趟循环都从数列的第一个元素开始进行比较,依次比较相邻的两个元素,比较到无序数列的末尾即可(而不是数列的末尾);如果前一个大于后一个,交换。
5.判断每一趟是否发生了数组元素的交换,如果没有发生,则说明此时数组已经有序,无需再进行后续趟数的比较了。此时可以中止比较。
【示例7-21】冒泡排序的优化算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 import java.util.Arrays;
public class Test1 {
public static void main(String[] args) {
int[] values = { 3, 1, 6, 2, 9, 0, 7, 4, 5, 8 };
bubbleSort(values);
System.out.println(Arrays.toString(values));
}
public static void bubbleSort(int[] values) {
int temp;
int i;
// 外层循环:n个元素排序,则至多需要n-1趟循环
for (i = 0; i < values.length - 1; i++) {
// 定义一个布尔类型的变量,标记数组是否已达到有序状态
boolean flag = true;
/内层循环:每一趟循环都从数列的前两个元素开始进行比较,比较到无序数组的最后/
for (int j = 0; j < values.length - 1 - i; j++) {
// 如果前一个元素大于后一个元素,则交换两元素的值;
if (values[j] > values[j + 1]) {
temp = values[j];
values[j] = values[j + 1];
values[j + 1] = temp;
//本趟发生了交换,表明该数组在本趟处于无序状态,需要继续比较;
flag = false;
}
}
//根据标记量的值判断数组是否有序,如果有序,则退出;无序,则继续循环。
if (flag) {
break;
}
}
}
}
执行结果如图7-15所示:
图7-15 示例7-21运行效果图
7.7 二分法查找
二分法检索(binary search)又称折半检索,二分法检索的基本思想是设数组中的元素从小到大有序地存放在数组(array)中,首先将给定值key与数组中间位置上元素的关键码(key)比较,如果相等,则检索成功;
否则,若key小,则在数组前半部分中继续进行二分法检索;
若key大,则在数组后半部分中继续进行二分法检索。
这样,经过一次比较就缩小一半的检索区间,如此进行下去,直到检索成功或检索失败。
二分法检索是一种效率较高的检索方法。比如,我们要在数组[7, 8, 9, 10, 12, 20, 30, 40, 50, 80, 100]中查询到10元素,过程如下:
图7-16 二分法示意图
【示例7-22】二分法查找
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = { 30,20,50,10,80,9,7,12,100,40,8};
int searchWord = 20; // 所要查找的数
Arrays.sort(arr); //二分法查找之前,一定要对数组元素排序
System.out.println(Arrays.toString(arr));
System.out.println(searchWord+“元素的索引:”+binarySearch(arr,searchWord));
}
public static int binarySearch(int[] array, int value){ int low = 0; int high = array.length - 1; while(low <= high){ int middle = (low + high) / 2; if(value == array[middle]){ return middle; //返回查询到的索引位置 } if(value > array[middle]){ low = middle + 1; } if(value < array[middle]){ high = middle - 1; } } return -1; //上面循环完毕,说明未找到,返回-1 }
}
执行结果如图7-17所示:
图7-17 示例7-22运行效果图
第七章 总结
第七章 作业
一、 选择题
1.在Java中,以下程序段能正确为数组赋值的是( )。(选择二项) AD
A.
1 int a[]={1,2,3,4};
B.
1 int b[4]={1,2,3,4};
C.
1
2 int c[];
c=new int[] {1,2,3,4};
D.
1 int d[];d=new int[]{1,2,3,4};
2.已知表达式int [] m={0,1,2,3,4,5,6};下面( )表达式的值与数组最大下标数相等。(选择一项) B
A.
1 m.length()
B.
1 m.length-1
C.
1 m.length()+1
D.
1 m.length+1
3.在Java中,以下定义数组的语句正确的是( )。(选择二项) C
A.
1 int t[10]=new int[ ];
B.
1
2 char [ ]a=new char[5];
char []a={‘a’,’b’};
C.
1 String [ ] s=new String [10];
D.
1
2
3 double[ ] d [ ]=new double [4][ ];
double[][] d;
double d[][];
4.分析下面的Java源程序,编译后的运行结果是( )。(选择一项)B
1
2
3
4
5
6
7 import java.util.*;
public class Test {
public static void main(String[ ] args) {
int [ ] numbers=new int[ ]{1,2,3};
System.out.println(Arrays.binarySearch(numbers, 2));
}
}
A.输出:0
B.输出:1
C.输出:2
D.输出:3
5.以下选项中能够正确创建一个数组的是( )。(选择二项)ad
A.
1 float []f[] = new float[6][6];
B.
1 float f[][] = new float[][];
C.
1 float [6][]f = new float[6][6];
D.
1 float [][]f = new float[6][];
二、 简答题
数组的特点。
长度是确定的。数组一旦被创建,它的大小就是不可以改变的。
其元素必须是相同类型,不允许出现混合类型。
数组类型可以是任何数据类型,包括基本类型和引用类型。
3.数组的优缺点
不可扩展数组长度但是访问速度较快。
4.冒泡排序的算法。
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] values = { 3, 1, 6, 2, 9, 0, 7, 4, 5, 8 };
bubbleSort(values);
System.out.println(Arrays.toString(values));
}
public static void bubbleSort(int[] values) {
int temp;
for (int i = 0; i < values.length; i++) {
for (int j = 0; j < values.length - 1 - i; j++) {
if (values[j] > values[j + 1]) {
temp = values[j];
values[j] = values[j + 1];
values[j + 1] = temp;
}
}
}
}
}
5.数组的三种初始化方式是什么?
静态初始化
int[] a = { 1, 2, 3 };// 静态初始化基本类型数组;
Man[] mans = { new Man(1, 1), new Man(2, 2) };// 静态初始化引用类型数组;
1
2
动态初始化
数组定义与为数组元素分配空间并赋值的操作分开进行。
1
默认初始化
int a2[] = new int[2]; // 默认值:0,0
boolean[] b = new boolean[2]; // 默认值:false,false
String[] s = new String[2]; // 默认值:null, null
三、 编码题
public class Seventh1 {
public static void main(String[] args) {
String [] vocabulary = {“aaa”,“bbb”,“ccc”,“ddd”,“eee”,“fff”,“ggg”};
Scanner scanner = new Scanner(System.in);
System.out.println(“请输入要搜索的单词:”);
String word = scanner.next();
if (Arrays.binarySearch(vocabulary, word) != -2) {
System.out.println(“搜索结果:YES”);
}else {
System.out.println(“搜索结果:NO”);
}
}
}
public class Seventh2 {
public static void main(String[] args) {
int n = 100;
int count = 0;
int[] nums = new int[50];
for(int i=0;i<50;i++) {
nums[i] = (int)(n*Math.random());
}
Arrays.sort(nums);
System.out.println("最小的数为:"+nums[0]);
System.out.println("最小的数为:"+nums[49]);
for(int i=0;i<50;i++) {
if(nums[i]>60) {
count++;
}
}
System.out.println("大于60的数有:"+count+"个");
}
}
、3. 数组逆序操作:定义长度为10的数组,将数组元素对调,并输出对调前后的结果。
思路:把0索引和arr.length-1的元素交换,把1索引和arr.length-2的元素交换……
只要交换到arr.length/2的时候即可。
public class Seventh3 {
public static void main(String[] args) {
int[] nums = {1,2,3,4,5,6,7,8,9,10};
reverse(nums);
for(int num:nums) {
System.out.print(num+" ");
}
}
static void reverse(int[] nums) {
int k;
for(int i=0;i<nums.length/2;i++) {
k=nums[i];
nums[i]=nums[nums.length-(i+1)];
nums[nums.length-(i+1)]=k;
}
}
}
第八章 常用类
8.1.1包装类基本知识
Java是面向对象的语言,但并不是“纯面向对象”的,因为我们经常用到的基本数据类型就不是对象。但是我们在实际应用中经常需要将基本数据转化成对象,以便于操作。比如:将基本数据类型存储到Object[]数组或集合中的操作等等。
为了解决这个不足,Java在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。
包装类均位于java.lang包,八种包装类和基本数据类型的对应关系如表8-1所示:
在这八个类名中,除了Integer和Character类以外,其它六个类的类名和基本数据类型一致,只是类名的第一个字母大写而已。
在这八个类中,除了Character和Boolean以外,其他的都是“数字型”,“数字型”都是java.lang.Number的子类。Number类是抽象类,因此它的抽象方法,所有子类都需要提供实现。Number类提供了抽象方法:intValue()、longValue()、floatValue()、doubleValue(),意味着所有的“数字型”包装类都可以互相转型。如图8-1和图8-2所示。
图8-1 Number类的子类
图8-2 Number类的抽象方法
下面我们通过一个简单的示例认识一下包装类。
【示例8-1】初识包装类
1
2
3
4
5
6 public class WrapperClassTest {
public static void main(String[] args) {
Integer i = new Integer(10);
Integer j = new Integer(50);
}
}
示例8-1的内存分析如图8-3所示:
图8-3 示例8-1内存分析图
8.1.2包装类的用途
s对于包装类来说,这些类的用途主要包含两种:
1. 作为和基本数据类型对应的类型存在,方便涉及到对象的操作,如Object[]、集合等的操作。
2. 包含每种基本数据类型的相关属性如最大值、最小值等,以及相关的操作方法(这些操作方法的作用是在基本数据类型、包装类对象、字符串之间提供相互之间的转化!)。
【示例8-2】包装类的使用
public class Test {
/** 测试Integer的用法,其他包装类与Integer类似 */
void testInteger() {
// 基本类型转化成Integer对象
Integer int1 = new Integer(10);
Integer int2 = Integer.valueOf(20); // 官方推荐这种写法
// Integer对象转化成int
int a = int1.intValue();
// 字符串转化成Integer对象
Integer int3 = Integer.parseInt(“334”);
Integer int4 = new Integer(“999”);
// Integer对象转化成字符串
String str1 = int3.toString();
// 一些常见int类型相关的常量
System.out.println(“int能表示的最大整数:” + Integer.MAX_VALUE);
}
public static void main(String[] args) {
Test test = new Test();
test.testInteger();
}
}
执行结果如图8-4所示:
图8-4 示例8-2运行效果图
8.1.3自动装箱和拆箱
自动装箱和拆箱就是将基本数据类型和包装类之间进行自动的互相转换。JDK1.5后,Java引入了自动装箱(autoboxing)/拆箱(unboxing)。
自动装箱:
基本类型的数据处于需要对象的环境中时,会自动转为“对象”。
我们以Integer为例:在JDK1.5以前,这样的代码 Integer i = 5 是错误的,必须要通过Integer i = new Integer(5) 这样的语句来实现基本数据类型转换成包装类的过程;而在JDK1.5以后,Java提供了自动装箱的功能,因此只需Integer i = 5这样的语句就能实现基本数据类型转换成包装类,这是因为JVM为我们执行了Integer i = Integer.valueOf(5)这样的操作,这就是Java的自动装箱。
自动拆箱:
每当需要一个值时,对象会自动转成基本数据类型,没必要再去显式调用intValue()、doubleValue()等转型方法。
如 Integer i = 5;int j = i; 这样的过程就是自动拆箱。
我们可以用一句话总结自动装箱/拆箱:
自动装箱过程是通过调用包装类的valueOf()方法实现的,而自动拆箱过程是通过调用包装类的 xxxValue()方法实现的(xxx代表对应的基本数据类型,如intValue()、doubleValue()等)。
自动装箱与拆箱的功能事实上是编译器来帮的忙,编译器在编译时依据您所编写的语法,决定是否进行装箱或拆箱动作,如示例8-3与示例8-4所示。
【示例8-3】自动装箱
1
2
3 Integer i = 100;//自动装箱
//相当于编译器自动为您作以下的语法编译:
Integer i = Integer.valueOf(100);//调用的是valueOf(100),而不是new Integer(100)
【示例8-4】自动拆箱
1
2
3
4 Integer i = 100;
int j = i;//自动拆箱
//相当于编译器自动为您作以下的语法编译:
int j = i.intValue();
所以自动装箱与拆箱的功能是所谓的“编译器蜜糖(Compiler Sugar)”,虽然使用这个功能很方便,但在程序运行阶段您得了解Java的语义。例如示例8-5所示的程序是可以通过编译的:
【示例8-5】包装类空指针异常问题
1
2
3
4
5
6 public class Test1 {
public static void main(String[] args) {
Integer i = null;
int j = i;
}
}
执行结果如图8-5所示:
图8-5 示例8-5运行效果图
示例8-5的运行结果之所以会出现空指针异常,是因为示例8-5中的代码相当于:
1
2
3
4
5
6
7 public class Test1 {
public static void main(String[] args) {
//示例8-5的代码在编译时期是合法的,但是在运行时期会有错误,因为其相当于:
Integer i = null;
int j = i.intValue();
}
}
null表示i没有指向任何对象的实体,但作为对象名称是合法的(不管这个对象名称存是否指向了某个对象的实体)。由于实际上i并没有指向任何对象的实体,所以也就不可能操作intValue()方法,这样上面的写法在运行时就会出现NullPointerException错误。
【示例8-6】自动装箱与拆箱
1
2
3
4
5
6
7
8
9
10
11
12 public class Test2 {
/**
* 测试自动装箱和拆箱 结论:虽然很方便,但是如果不熟悉特殊情况,可能会出错!
*/
public static void main(String[] args) {
Integer b = 23; // 自动装箱
int a = new Integer(20); //自动拆箱
// 下面的问题我们需要注意:
Integer c = null;
int d = c; // 此处其实就是:c.intValue(),因此抛空指针异常。
}
}
8.1.4包装类的缓存问题
整型、char类型所对应的包装类,在自动装箱时,对于-128~127之间的值会进行缓存处理,其目的是提高效率。
缓存处理的原理为:如果数据在-128~127这个区间,那么在类加载时就已经为该区间的每个数值创建了对象,并将这256个对象存放到一个名为cache的数组中。每当自动装箱过程发生时(或者手动调用valueOf()时),就会先判断数据是否在该区间,如果在则直接获取数组中对应的包装类对象的引用,如果不在该区间,则会通过new调用包装类的构造方法来创建对象。
下面我们以Integer类为例,看一看Java为我们提供的源码,加深对缓存技术的理解,如示例8-7所示。
【示例8-7】Integer类相关源码如下:
1
2
3
4
5 public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
这段代码中我们需要解释下面几个问题:
1. IntegerCache类为Integer类的一个静态内部类,仅供Integer类使用。
2. 一般情况下 IntegerCache.low为-128,IntegerCache.high为127,IntegerCache.cache为内部类的一个静态属性,如示例8-8所示。
【示例8-8】IntegerCache类相关源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty(“java.lang.Integer.IntegerCache.high”);
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
由上面的源码我们可以看到,静态代码块的目的就是初始化数组cache的,这个过程会在类加载时完成。
下面我们做一下代码测试,如示例8-9所示。
【示例8-9】测试代码
1
2
3
4
5
6
7
8
9
10
11
12 public class Test3 {
public static void main(String[] args) {
Integer in1 = -128;
Integer in2 = -128;
System.out.println(in1 == in2);//true 因为123在缓存范围内
System.out.println(in1.equals(in2));//true
Integer in3 = 1234;
Integer in4 = 1234;
System.out.println(in3 == in4);//false 因为1234不在缓存范围内
System.out.println(in3.equals(in4));//true
}
}
执行结果如图8-6所示:
图8-6 示例8-9运行效果图
示例8-9的内存分析如图8-7所示:
图8-7 示例8-9的内存分析图
注意
1. JDK1.5以后,增加了自动装箱与拆箱功能,如:
1 Integer i = 100; int j = new Integer(100);
2. 自动装箱调用的是valueOf()方法,而不是new Integer()方法。
3. 自动拆箱调用的xxxValue()方法。
4. 包装类在自动装箱时为了提高效率,对于-128~127之间的值会进行缓存处理。超过范围后,对象之间不能再使用==进行数值的比较,而是使用equals方法。
8.2.1 String类
String 类对象代表不可变的Unicode字符序列,因此我们可以将String对象称为“不可变对象”。 那什么叫做“不可变对象”呢?指的是对象内部的成员变量的值无法再改变。我们打开String类的源码,如图8-8所示:
图8-8 String类的部分源码
我们发现字符串内容全部存储到value[]数组中,而变量value是final类型的,也就是常量(即只能被赋值一次)。 这就是“不可变对象”的典型定义方式。
我们发现在前面学习String的某些方法,比如:substring()是对字符串的截取操作,但本质是读取原字符串内容生成了新的字符串。测试代码如下:
【示例8-10】String测试代码
public class TestString1 {
public static void main(String[] args) {
String s1 = new String(“abcdef”);
String s2 = s1.substring(2, 4);
// 打印:ab199863
System.out.println(Integer.toHexString(s1.hashCode()));
// 打印:c61, 显然s1和s2不是同一个对象
System.out.println(Integer.toHexString(s2.hashCode()));
}
}
执行结果如图8-9所示:
图8-9 示例8-10运行效果图
在遇到字符串常量之间的拼接时,编译器会做出优化,即在编译期间就会完成字符串的拼接。因此,在使用==进行String对象之间的比较时,我们需要特别注意,如示例8-11所示。
【示例8-11】字符串常量拼接时的优化
public class TestString2 {
public static void main(String[] args) {
//编译器做了优化,直接在编译的时候将字符串进行拼接
String str1 = “hello” + " java";//相当于str1 = “hello java”;
String str2 = “hello java”;
System.out.println(str1 == str2);//true
String str3 = “hello”;
String str4 = " java";
//编译的时候不知道变量中存储的是什么,所以没办法在编译的时候优化
String str5 = str3 + str4;
System.out.println(str2 == str5);//false
}
}
执行结果如图8-10所示:
图8-10 示例8-11运行效果图
String类常用的方法有(可翻到第五章5.11.4查看,已讲过,此处不赘述):
1. String类的下述方法能创建并返回一个新的String对象: concat()、 replace()、substring()、 toLowerCase()、 toUpperCase()、trim()。
2. 提供查找功能的有关方法: endsWith()、 startsWith()、 indexOf()、lastIndexOf()。
3. 提供比较功能的方法: equals()、equalsIgnoreCase()、compareTo()。
4. 其它方法: charAt() 、length()。
8.2.3 不可变和可变字符序列使用陷阱
· String使用的陷阱
String一经初始化后,就不会再改变其内容了。对String字符串的操作实际上是对其副本(原始拷贝)的操作,原来的字符串一点都没有改变。比如:
String s =“a”; 创建了一个字符串
s = s+“b”; 实际上原来的"a"字符串对象已经丢弃了,现在又产生了另一个字符串s+“b”(也就是"ab")。 如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的时间和空间性能,甚至会造成服务器的崩溃。
相反,StringBuilder和StringBuffer类是对原字符串本身操作的,可以对字符串进行修改而不产生副本拷贝或者产生少量的副本。因此可以在循环中使用。
【示例8-13】String和StringBuilder在频繁字符串修改时效率测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 public class Test {
public static void main(String[] args) {
/*使用String进行字符串的拼接/
String str8 = “”;
//本质上使用StringBuilder拼接, 但是每次循环都会生成一个StringBuilder对象
long num1 = Runtime.getRuntime().freeMemory();//获取系统剩余内存空间
long time1 = System.currentTimeMillis();//获取系统的当前时间
for (int i = 0; i < 5000; i++) {
str8 = str8 + i;//相当于产生了10000个对象
}
long num2 = Runtime.getRuntime().freeMemory();
long time2 = System.currentTimeMillis();
System.out.println(“String占用内存 : " + (num1 - num2));
System.out.println(“String占用时间 : " + (time2 - time1));
/*使用StringBuilder进行字符串的拼接/
StringBuilder sb1 = new StringBuilder(””);
long num3 = Runtime.getRuntime().freeMemory();
long time3 = System.currentTimeMillis();
for (int i = 0; i < 5000; i++) {
sb1.append(i);
}
long num4 = Runtime.getRuntime().freeMemory();
long time4 = System.currentTimeMillis();
System.out.println("StringBuilder占用内存 : " + (num3 - num4));
System.out.println("StringBuilder占用时间 : " + (time4 - time3));
}
}
执行结果如图8-12所示:
图8-12 示例8-13运行效果图
要点:
1. String:不可变字符序列。
2. StringBuffer:可变字符序列,并且线程安全,但是效率低。
3. StringBuilder:可变字符序列,线程不安全,但是效率高(一般用它)。
8.3 时间处理相关类
“时间如流水,一去不复返”,时间是一个一维的东东。所以,我们需要一把刻度尺来表达和度量时间。在计算机世界,我们把1970 年 1 月 1 日 00:00:00定为基准时间,每个度量单位是毫秒(1秒的千分之一),如图8-13所示。
图8-13 计算机的时间概念
我们用long类型的变量来表示时间,从基准时间往前几亿年,往后几亿年都能表示。如果想获得现在时刻的“时刻数值”,可以使用:
1 long now = System.currentTimeMillis();
这个“时刻数值”是所有时间类的核心值,年月日都是根据这个“数值”计算出来的。我们工作学习涉及的时间相关类有如下这些:
图8-14 日期时间相关类
8.3.1 Date时间类(java.util.Date)
在标准Java类库中包含一个Date类。它的对象表示一个特定的瞬间,精确到毫秒。
1. Date() 分配一个Date对象,并初始化此对象为系统当前的日期和时间,可以精确到毫秒)。
2. Date(long date) 分配 Date 对象并初始化此对象,以表示自从标准基准时间(称为“历元(epoch)”,即 1970 年 1 月 1 日 00:00:00 GMT)以来的指定毫秒数。
3. boolean after(Date when) 测试此日期是否在指定日期之后。
4. booleanbefore(Date when) 测试此日期是否在指定日期之前。
5. boolean equals(Object obj) 比较两个日期的相等性。
6. long getTime() 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。
7. String toString() 把此 Date 对象转换为以下形式的 String:
dow mon dd hh:mm:ss zzz yyyy 其中: dow 是一周中的某一天 (Sun、 Mon、Tue、Wed、 Thu、 Fri、 Sat)。
【示例8-14】Date类的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import java.util.Date;
public class TestDate {
public static void main(String[] args) {
Date date1 = new Date();
System.out.println(date1.toString());
long i = date1.getTime();
Date date2 = new Date(i - 1000);
Date date3 = new Date(i + 1000);
System.out.println(date1.after(date2));
System.out.println(date1.before(date2));
System.out.println(date1.equals(date2));
System.out.println(date1.after(date3));
System.out.println(date1.before(date3));
System.out.println(date1.equals(date3));
System.out.println(new Date(1000L * 60 * 60 * 24 * 365 * 39L).toString());
}
}
执行结果如图8-15所示:
图8-15 示例8-14运行效果图
查看API文档大家可以看到其实Date类中的很多方法都已经过时了。JDK1.1之前的Date包含了:日期操作、字符串转化成时间对象等操作。JDK1.1之后,日期操作一般使用Calendar类,而字符串的转化使用DateFormat类。
8.3.2 DateFormat类和SimpleDateFormat类
·DateFormat类的作用
把时间对象转化成指定格式的字符串。反之,把指定格式的字符串转化成时间对象。
DateFormat是一个抽象类,一般使用它的的子类SimpleDateFormat类来实现。
【示例8-15】DateFormat类和SimpleDateFormat类的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TestDateFormat {
public static void main(String[] args) throws ParseException {
// new出SimpleDateFormat对象
SimpleDateFormat s1 = new SimpleDateFormat(“yyyy-MM-dd hh:mm:ss”);
SimpleDateFormat s2 = new SimpleDateFormat(“yyyy-MM-dd”);
// 将时间对象转换成字符串
String daytime = s1.format(new Date());
System.out.println(daytime);
System.out.println(s2.format(new Date()));
System.out.println(new SimpleDateFormat(“hh:mm:ss”).format(new Date()));
// 将符合指定格式的字符串转成成时间对象.字符串格式需要和指定格式一致。
String time = “2007-10-7”;
Date date = s2.parse(time);
System.out.println("date1: " + date);
time = “2007-10-7 20:15:30”;
date = s1.parse(time);
System.out.println("date2: " + date);
}
}
执行结果如图8-16所示:
图8-16 示例8-15运行效果图
代码中的格式化字符的具体含义见表8-2:
时间格式字符也可以为我们提供其他的便利。比如:获得当前时间是今年的第几天。代码如下:
【示例8-16】时间格式字符的使用
1
2
3
4
5
6
7
8
9 import java.text.SimpleDateFormat;
import java.util.Date;
public class TestDateFormat2 {
public static void main(String[] args) {
SimpleDateFormat s1 = new SimpleDateFormat(“D”);
String daytime = s1.format(new Date());
System.out.println(daytime);
}
}
执行结果如图8-17所示:
图8-17 示例8-16运行效果图
8.3.3 Calendar日历类
Calendar 类是一个抽象类,为我们提供了关于日期计算的相关功能,比如:年、月、日、时、分、秒的展示和计算。
GregorianCalendar 是 Calendar 的一个具体子类,提供了世界上大多数国家/地区使用的标准日历系统。
菜鸟雷区
注意月份的表示,一月是0,二月是1,以此类推,12月是11。 因为大多数人习惯于使用单词而不是使用数字来表示月份,这样程序也许更易读,父类Calendar使用常量来表示月份:JANUARY、FEBRUARY等等。
【示例8-17】GregorianCalendar类和Calendar类的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 import java.util.*;
public class TestCalendar {
public static void main(String[] args) {
// 得到相关日期元素
GregorianCalendar calendar = new GregorianCalendar(2999, 10, 9, 22, 10, 50);
int year = calendar.get(Calendar.YEAR); // 打印:1999
int month = calendar.get(Calendar.MONTH); // 打印:10
int day = calendar.get(Calendar.DAY_OF_MONTH); // 打印:9
int day2 = calendar.get(Calendar.DATE); // 打印:9
// 日:Calendar.DATE和Calendar.DAY_OF_MONTH同义
int date = calendar.get(Calendar.DAY_OF_WEEK); // 打印:3
// 星期几 这里是:1-7.周日是1,周一是2,。。。周六是7
System.out.println(year);
System.out.println(month);
System.out.println(day);
System.out.println(day2);
System.out.println(date);
// 设置日期
GregorianCalendar calendar2 = new GregorianCalendar();
calendar2.set(Calendar.YEAR, 2999);
calendar2.set(Calendar.MONTH, Calendar.FEBRUARY); // 月份数:0-11
calendar2.set(Calendar.DATE, 3);
calendar2.set(Calendar.HOUR_OF_DAY, 10);
calendar2.set(Calendar.MINUTE, 20);
calendar2.set(Calendar.SECOND, 23);
printCalendar(calendar2);
// 日期计算
GregorianCalendar calendar3 = new GregorianCalendar(2999, 10, 9, 22, 10, 50);
calendar3.add(Calendar.MONTH, -7); // 月份减7
calendar3.add(Calendar.DATE, 7); // 增加7天
printCalendar(calendar3);
// 日历对象和时间对象转化
Date d = calendar3.getTime();
GregorianCalendar calendar4 = new GregorianCalendar();
calendar4.setTime(new Date());
long g = System.currentTimeMillis();
}
static void printCalendar(Calendar calendar) {
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1;
int day = calendar.get(Calendar.DAY_OF_MONTH);
int date = calendar.get(Calendar.DAY_OF_WEEK) - 1; // 星期几
String week = “” + ((date == 0) ? “日” : date);
int hour = calendar.get(Calendar.HOUR);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
System.out.printf(“%d年%d月%d日,星期%s %d:%d:%d\n”, year, month, day,
week, hour, minute, second);
}
}
执行结果如图8-18所示:
图8-18 示例8-17运行效果图
编写程序,利用GregorianCalendar类,打印当前月份的日历,今天的日期是 2017-05-18 ,如图8-19所示为今日所在月份的日历:
图8-19 示例8-18运行效果图
【示例8-18】可视化日历的编写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49 import java.text.ParseException;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Scanner;
public class TestCalendar2 {
public static void main(String[] args) throws ParseException {
System.out.println(“请输入日期(格式为:2010-3-3):”);
Scanner scanner = new Scanner(System.in);
String dateString = scanner.nextLine(); // 2010-3-1
// 将输入的字符串转化成日期类
System.out.println(“您刚刚输入的日期是:” + dateString);
String[] str = dateString.split(“-”);
int year = Integer.parseInt(str[0]);
int month = new Integer(str[1]);
int day = new Integer(str[2]);
Calendar c = new GregorianCalendar(year, month - 1, day); // Month:0-11
// 大家自己补充另一种方式:将字符串通过SImpleDateFormat转化成Date对象,
//再将Date对象转化成日期类
// SimpleDateFormat sdfDateFormat = new SimpleDateFormat(“yyyy-MM-dd”);
// Date date = sdfDateFormat.parse(dateString);
// Calendar c = new GregorianCalendar();
// c.setTime(date);
// int day = c.get(Calendar.DATE);
c.set(Calendar.DATE, 1);
int dow = c.get(Calendar.DAY_OF_WEEK); // week:1-7 日一二三四五六
System.out.println(“日\t一\t二\t三\t四\t五\t六”);
for (int i = 0; i < dow - 1; i++) {
System.out.print(“\t”);
}
int maxDate = c.getActualMaximum(Calendar.DATE);
// System.out.println(“maxDate:”+maxDate);
for (int i = 1; i <= maxDate; i++) {
StringBuilder sBuilder = new StringBuilder();
if (c.get(Calendar.DATE) == day) {
sBuilder.append(c.get(Calendar.DATE) + “\t");
} else {
sBuilder.append(c.get(Calendar.DATE) + “\t”);
}
System.out.print(sBuilder);
// System.out.print(c.get(Calendar.DATE)+
// ((c.get(Calendar.DATE)==day)?"”:“”)+“\t”);
if (c.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {
System.out.print("\n");
}
c.add(Calendar.DATE, 1);
}
}
}
8.4 Math类
java.lang.Math提供了一系列静态方法用于科学计算;其方法的参数和返回值类型一般为double型。如果需要更加强大的数学运算能力,计算高等数学中的相关内容,可以使用apache commons下面的Math类库。
Math类的常用方法:
1. abs 绝对值
2. acos,asin,atan,cos,sin,tan 三角函数
3. sqrt 平方根
4. pow(double a, double b) a的b次幂
5. max(double a, double b) 取大值
6. min(double a, double b) 取小值
7. ceil(double a) 大于a的最小整数
8. floor(double a) 小于a的最大整数
9. random() 返回 0.0 到 1.0 的随机数
10. long round(double a) double型的数据a转换为long型(四舍五入)
11. toDegrees(double angrad) 弧度->角度
12. toRadians(double angdeg) 角度->弧度
【示例8-19】Math类的常用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 public class TestMath {
public static void main(String[] args) {
//取整相关操作
System.out.println(Math.ceil(3.2));
System.out.println(Math.floor(3.2));
System.out.println(Math.round(3.2));
System.out.println(Math.round(3.8));
//绝对值、开方、a的b次幂等操作
System.out.println(Math.abs(-45));
System.out.println(Math.sqrt(64));
System.out.println(Math.pow(5, 2));
System.out.println(Math.pow(2, 5));
//Math类中常用的常量
System.out.println(Math.PI);
System.out.println(Math.E);
//随机数
System.out.println(Math.random());// [0,1)
}
}
执行结果如图8-20所示:
图8-20 示例8-19运行效果图
Math类中虽然为我们提供了产生随机数的方法Math.random(),但是通常我们需要的随机数范围并不是[0, 1)之间的double类型的数据,这就需要对其进行一些复杂的运算。如果使用Math.random()计算过于复杂的话,我们可以使用例外一种方式得到随机数,即Random类,这个类是专门用来生成随机数的,并且Math.random()底层调用的就是Random的nextDouble()方法。
【示例8-20】Random类的常用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 import java.util.Random;
public class TestRandom {
public static void main(String[] args) {
Random rand = new Random();
//随机生成[0,1)之间的double类型的数据
System.out.println(rand.nextDouble());
//随机生成int类型允许范围之内的整型数据
System.out.println(rand.nextInt());
//随机生成[0,1)之间的float类型的数据
System.out.println(rand.nextFloat());
//随机生成false或者true
System.out.println(rand.nextBoolean());
//随机生成[0,10)之间的int类型的数据
System.out.print(rand.nextInt(10));
//随机生成[20,30)之间的int类型的数据
System.out.print(20 + rand.nextInt(10));
//随机生成[20,30)之间的int类型的数据(此种方法计算较为复杂)
System.out.print(20 + (int) (rand.nextDouble() * 10));
}
}
执行结果如图8-21所示:
图8-21 示例8-20运行效果图
注意
Random类位于java.util包下。
8.5.1 File类的基本用法
java.io.File类:代表文件和目录。 在开发中,读取文件、生成文件、删除文件、修改文件的属性时经常会用到本类。
File类的常见构造方法:public File(String pathname)
以pathname为路径创建File对象,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储,如示例8-21所示。
【示例8-21】文件的创建
1
2
3
4
5
6
7
8
9
10 import java.io.File;
public class TestFile1 {
public static void main(String[] args) throws Exception {
System.out.println(System.getProperty(“user.dir”));
File f = new File(“a.txt”); //相对路径:默认放到user.dir目录下面
f.createNewFile();//创建文件
File f2 = new File(“d:/b.txt”);//绝对路径
f2.createNewFile();
}
}
在eclipse项目开发中,user.dir就是本项目的目录。因此,执行完毕后,在本项目和D盘下都生成了新的文件(如果是eclipse下,一定按F5刷新目录结构才能看到新文件)。如图8-22所示。
图8-22 本项目目录中新增文件效果
通过File对象可以访问文件的属性:
表8-3 File类访问属性的方法列表
【示例8-22】测试File类访问属性的基本用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14 import java.io.File;
import java.util.Date;
public class TestFile2 {
public static void main(String[] args) throws Exception {
File f = new File(“d:/b.txt”);
System.out.println(“File是否存在:”+f.exists());
System.out.println(“File是否是目录:”+f.isDirectory());
System.out.println(“File是否是文件:”+f.isFile());
System.out.println(“File最后修改时间:”+new Date(f.lastModified()));
System.out.println(“File的大小:”+f.length());
System.out.println(“File的文件名:”+f.getName());
System.out.println(“File的目录路径:”+f.getPath());
}
}
执行结果如图8-23所示:
图8-23 示例8-22运行效果图
通过File对象创建空文件或目录(在该对象所指的文件或目录不存在的情况下)
表8-4 File类创建文件或目录的方法列表
【示例8-23】使用mkdir创建目录
1
2
3
4
5
6
7
8
9
10
11 import java.io.File;
public class TestFile3 {
public static void main(String[] args) throws Exception {
File f = new File(“d:/c.txt”);
f.createNewFile(); // 会在d盘下面生成c.txt文件
f.delete(); // 将该文件或目录从硬盘上删除
File f2 = new File(“d:/电影/华语/大陆”);
boolean flag = f2.mkdir(); //目录结构中有一个不存在,则不会创建整个目录树
System.out.println(flag);//创建失败
}
}
执行结果如图8-24所示:
图8-24 示例8-23运行效果图
【示例8-24】使用mkdirs创建目录
1
2
3
4
5
6
7
8
9
10
11 import java.io.File;
public class TestFile4 {
public static void main(String[] args) throws Exception {
File f = new File(“d:/c.txt”);
f.createNewFile(); // 会在d盘下面生成c.txt文件
f.delete(); // 将该文件或目录从硬盘上删除
File f2 = new File(“d:/电影/华语/大陆”);
boolean flag = f2.mkdirs();//目录结构中有一个不存在也没关系;创建整个目录树
System.out.println(flag);//创建成功
}
}
执行结果如图8-25所示:
图8-25 示例8-24运行效果图
【示例8-25】File类的综合应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 import java.io.File;
import java.io.IOException;
public class TestFile5 {
public static void main(String[] args) {
//指定一个文件
File file = new File(“d:/sxt/b.txt”);
//判断该文件是否存在
boolean flag= file.exists();
//如果存在就删除,如果不存在就创建
if(flag){
//删除
boolean flagd = file.delete();
if(flagd){
System.out.println(“删除成功”);
}else{
System.out.println(“删除失败”);
}
}else{
//创建
boolean flagn = true;
try {
//如果目录不存在,先创建目录
File dir = file.getParentFile();
dir.mkdirs();
//创建文件
flagn = file.createNewFile();
System.out.println(“创建成功”);
} catch (IOException e) {
System.out.println(“创建失败”);
e.printStackTrace();
}
}
//文件重命名(同学可以自己测试一下)
//file.renameTo(new File(“d:/readme.txt”));
}
}
第一次执行结果如图8-26所示:
图8-26 示例8-25第一次运行效果图
第二次执行结果如图8-27所示:
图8-27 示例8-25第二次运行效果图
8.5.2 递归遍历目录结构和树状展现
本节结合前面给大家讲的递归算法,展示目录结构。大家可以先建立一个目录,下面增加几个子文件夹或者文件,用于测试。
【示例8-26】使用递归算法,以树状结构展示目录树
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 import java.io.File;
public class TestFile6 {
public static void main(String[] args) {
File f = new File(“d:/电影”);
printFile(f, 0);
}
/**
* 打印文件信息
* @param file 文件名称
* @param level 层次数(实际就是:第几次递归调用)
*/
static void printFile(File file, int level) {
//输出层次数
for (int i = 0; i < level; i++) {
System.out.print(“-”);
}
//输出文件名
System.out.println(file.getName());
//如果file是目录,则获取子文件列表,并对每个子文件进行相同的操作
if (file.isDirectory()) {
File[] files = file.listFiles();
for (File temp : files) {
//递归调用该方法:注意等+1
printFile(temp, level + 1);
}
}
}
}
执行结果如图8-28所示:
图8-28 示例8-26运行效果图
8.6 枚举
JDK1.5引入了枚举类型。枚举类型的定义包括枚举声明和枚举体。格式如下:
1
2
3 enum 枚举名 {
枚举体(常量列表)
}
枚举体就是放置一些常量。我们可以写出我们的第一个枚举类型,如示例8-27所示:
【示例8-27】创建枚举类型
1
2
3 enum Season {
SPRING, SUMMER, AUTUMN, WINDER
}
所有的枚举类型隐性地继承自 java.lang.Enum。枚举实质上还是类!而每个被枚举的成员实质就是一个枚举类型的实例,他们默认都是public static final修饰的。可以直接通过枚举类型名使用它们。
老鸟建议
1. 当你需要定义一组常量时,可以使用枚举类型。
2. 尽量不要使用枚举的高级特性,事实上高级特性都可以使用普通类来实现,没有必要引入枚举,增加程序的复杂性!
【示例8-28】枚举的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 import java.util.Random;
public class TestEnum {
public static void main(String[] args) {
// 枚举遍历
for (Week k : Week.values()) {
System.out.println(k);
}
// switch语句中使用枚举
int a = new Random().nextInt(4); // 生成0,1,2,3的随机数
switch (Season.values()[a]) {
case SPRING:
System.out.println(“春天”);
break;
case SUMMER:
System.out.println(“夏天”);
break;
case AUTUMN:
System.out.println(“秋天”);
break;
case WINDTER:
System.out.println(“冬天”);
break;
}
}
}
/*季节/
enum Season {
SPRING, SUMMER, AUTUMN, WINDTER
}
/*星期/
enum Week {
星期一, 星期二, 星期三, 星期四, 星期五, 星期六, 星期日
}
第八章 总结
public class Eighth1 {
public static void main(String[] args) {
System.out.println(“请输入用户名:”);
Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine();
while(!isLegal(s)) {
System.out.println(“请重新输入用户名:”);
s = scanner.nextLine();
}
System.out.println(“输入正确”);
}
public static boolean isLegal(String s) { if (s.isEmpty()) { System.out.println("用户名不能为空"); return false; }else if (s.length()<6) { System.out.println("用户名长度不能小于6"); return false; }else if (hasNum(s)) { System.out.println("用户名中不可包含数字"); return false; } return true; } public static boolean hasNum(String s) { for(int i=0;i<s.length();i++) { if(Character.isDigit(s.charAt(i))) { return true; } } return false; }
}
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;
public class Eighth2 {
public static void main(String[] args) throws ParseException {
String age, score, admisionTime;
Scanner scanner = new Scanner(System.in);
System.out.println(“请输入您的年龄:”);
age = scanner.nextLine();
System.out.println(“请输入您的分数:”);
score = scanner.nextLine();
System.out.println(“请输入您入学时间(格式例:1999-9-9):”);
admisionTime = scanner.nextLine();
Integer age1 = Integer.valueOf(age);
Double score1 = Double.valueOf(score);
DateFormat d1 = new SimpleDateFormat("yyyy-mm-dd");
Date admisionTime1 = d1.parse(admisionTime);
System.out.println("年龄为:"+age1+",分数为:"+score1+",入学时间为:"+admisionTime);
}
}
根据交通信号灯颜色决定汽车停车、行驶和慢行
提示:使用枚举实现
public class Eighth3 {
public static void main(String[] args) throws InterruptedException {
boolean flag = true;
while(flag) {
for(int i=0;i<3;i++) {
switch (TrafficLight.values()[i]) {
case 红灯:
System.out.println("红灯:汽车停止");
break;
case 黄灯:
System.out.println("黄灯:汽车慢行");
break;
case 绿灯:
System.out.println("绿灯:汽车通行");
break;
}
Thread.sleep(3000);
}
}
}
}
enum TrafficLight{
红灯,黄灯,绿灯
}
第九章 容器
9.1 泛型Generics
开发和学习中需要时刻和数据打交道,如何组织这些数据是我们编程中重要的内容。 我们一般通过“容器”来容纳和管理数据。那什么是“容器”呢?生活中的容器不难理解,是用来容纳物体的,如锅碗瓢盆、箱子和包等。程序中的“容器”也有类似的功能,就是用来容纳和管理数据。
事实上,我们第七章所学的数组就是一种容器,可以在其中放置对象或基本类型数据。
数组的优势:是一种简单的线性序列,可以快速地访问数组元素,效率高。如果从效率和类型检查的角度讲,数组是最好的。
数组的劣势:不灵活。容量需要事先定义好,不能随着需求的变化而扩容。比如:我们在一个用户管理系统中,要把今天注册的所有用户取出来,那么这样的用户有多少个?我们在写程序时是无法确定的。因此,在这里就不能使用数组。
基于数组并不能满足我们对于“管理和组织数据的需求”,所以我们需要一种更强大、更灵活、容量随时可扩的容器来装载我们的对象。 这就是我们今天要学习的容器,也叫集合(Collection)。以下是容器的接口层次结构图:
图9-1容器的接口层次结构图
为了能够更好的学习容器,我们首先要先来学习一个概念:泛型。
泛型是JDK1.5以后增加的,它可以帮助我们建立类型安全的集合。在使用了泛型的集合中,遍历时不必进行强制类型转换。JDK提供了支持泛型的编译器,将运行时的类型检查提前到了编译时执行,提高了代码可读性和安全性。
泛型的本质就是“数据类型的参数化”。 我们可以把“泛型”理解为数据类型的一个占位符(形式参数),即告诉编译器,在调用泛型时必须传入实际类型。
9.1.1 自定义泛型
我们可以在类的声明处增加泛型列表,如:<T,E,V>。
此处,字符可以是任何标识符,一般采用这3个字母。
【示例9-1】泛型类的声明
1
2
3
4
5
6
7
8
9
10 class MyCollection {// E:表示泛型;
Object[] objs = new Object[5];
public E get(int index) {// E:表示泛型;
return (E) objs[index];
}
public void set(E e, int index) {// E:表示泛型;
objs[index] = e;
}
}
泛型E像一个占位符一样表示“未知的某个数据类型”,我们在真正调用的时候传入这个“数据类型”。
【示例9-2】泛型类的应用
1
2
3
4
5
6
7
8
9
10 public class TestGenerics {
public static void main(String[] args) {
// 这里的”String”就是实际传入的数据类型;
MyCollection mc = new MyCollection();
mc.set(“aaa”, 0);
mc.set(“bbb”, 1);
String str = mc.get(1); //加了泛型,直接返回String类型,不用强制转换;
System.out.println(str);
}
}
9.1.2 容器中使用泛型
容器相关类都定义了泛型,我们在开发和工作中,在使用容器类时都要使用泛型。这样,在容器的存储数据、读取数据时都避免了大量的类型判断,非常便捷。
【示例9-3】泛型类的在集合中的使用
1
2
3
4
5
6
7
8
9 public class Test {
public static void main(String[] args) {
// 以下代码中List、Set、Map、Iterator都是与容器相关的接口;
List list = new ArrayList();
Set mans = new HashSet();
Map<Integer, Man> maps = new HashMap<Integer, Man>();
Iterator iterator = mans.iterator();
}
}
通过阅读源码,我们发现Collection、List、Set、Map、Iterator接口都定义了泛型,如下图所示:
图9-2容器的泛型定义
因此,我们在使用这些接口及其实现类时,都要使用泛型。
菜鸟雷区
我们只是强烈建议使用泛型。事实上,不使用编译器也不会报错!
9.2 Collection接口
【专业IT培训机构,真正零首付入学www.bjsxt.com】
Collection 表示一组对象,它是集中、收集的意思。Collection接口的两个子接口是List、Set接口。
表9-1 Collection接口中定义的方法
由于List、Set是Collection的子接口,意味着所有List、Set的实现类都有上面的方法。我们下一节中,通过ArrayList实现类来测试上面的方法。
9.3.1 List特点和常用方法
List是有序、可重复的容器。
有序:List中每个元素都有索引标记。可以根据元素的索引标记(在List中的位置)访问元素,从而精确控制这些元素。
可重复:List允许加入重复的元素。更确切地讲,List通常允许满足 e1.equals(e2) 的元素重复加入容器。
除了Collection接口中的方法,List多了一些跟顺序(索引)有关的方法,参见下表:
表9-2List接口中定义的方法
List接口常用的实现类有3个:ArrayList、LinkedList和Vector。
【示例9-4】List的常用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 public class TestList {
/**
* 测试add/remove/size/isEmpty/contains/clear/toArrays等方法
*/
public static void test01() {
List list = new ArrayList();
System.out.println(list.isEmpty()); // true,容器里面没有元素
list.add(“高淇”);
System.out.println(list.isEmpty()); // false,容器里面有元素
list.add(“高小七”);
list.add(“高小八”);
System.out.println(list);
System.out.println(“list的大小:” + list.size());
System.out.println(“是否包含指定元素:” + list.contains(“高小七”));
list.remove(“高淇”);
System.out.println(list);
Object[] objs = list.toArray();
System.out.println(“转化成Object数组:” + Arrays.toString(objs));
list.clear();
System.out.println(“清空所有元素:” + list);
}
public static void main(String[] args) {
test01();
}
}
执行结果如图9-3所示:
图9-3 示例9-4运行效果图
【示例9-5】两个List之间的元素处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 public class TestList {
public static void main(String[] args) {
test02();
}
/**
* 测试两个容器之间元素处理
*/
public static void test02() {
List list = new ArrayList();
list.add(“高淇”);
list.add(“高小七”);
list.add(“高小八”);
List<String> list2 = new ArrayList<String>();
list2.add("高淇");
list2.add("张三");
list2.add("李四");
System.out.println(list.containsAll(list2)); //false list是否包含list2中所有元素
System.out.println(list);
list.addAll(list2); //将list2中所有元素都添加到list中
System.out.println(list);
list.removeAll(list2); //从list中删除同时在list和list2中存在的元素
System.out.println(list);
list.retainAll(list2); //取list和list2的交集
System.out.println(list);
}
}
执行结果如图9-4所示:
图9-4 示例9-5运行效果图
【示例9-6】List中操作索引的常用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 public class TestList {
public static void main(String[] args) {
test03();
}
/**
* 测试List中关于索引操作的方法
*/
public static void test03() {
List list = new ArrayList();
list.add(“A”);
list.add(“B”);
list.add(“C”);
list.add(“D”);
System.out.println(list); // [A, B, C, D]
list.add(2, “高”);
System.out.println(list); // [A, B, 高, C, D]
list.remove(2);
System.out.println(list); // [A, B, C, D]
list.set(2, “c”);
System.out.println(list); // [A, B, c, D]
System.out.println(list.get(1)); // 返回:B
list.add(“B”);
System.out.println(list); // [A, B, c, D, B]
System.out.println(list.indexOf(“B”)); // 1 从头到尾找到第一个"B"
System.out.println(list.lastIndexOf(“B”)); // 4 从尾到头找到第一个"B"
}
}
执行结果如图9-5所示:
图9-5 示例9-6运行效果图
9.3.2 ArrayList特点和底层实现
ArrayList底层是用数组实现的存储。 特点:查询效率高,增删效率低,线程不安全。我们一般使用它。查看源码:
图9-6 ArrayList底层源码(1)
我们可以看出ArrayList底层使用Object数组来存储元素数据。所有的方法,都围绕这个核心的Object数组来开展。
我们知道,数组长度是有限的,而ArrayList是可以存放任意数量的对象,长度不受限制,那么它是怎么实现的呢?本质上就是通过定义新的更大的数组,将旧数组中的内容拷贝到新数组,来实现扩容。 ArrayList的Object数组初始化长度为10,如果我们存储满了这个数组,需要存储第11个对象,就会定义新的长度更大的数组,并将原数组内容和新的元素一起加入到新数组中,源码如下:
图9-7 ArrayList底层源码(2)
9.3.3 LinkedList特点和底层实现
LinkedList底层用双向链表实现的存储。特点:查询效率低,增删效率高,线程不安全。
双向链表也叫双链表,是链表的一种,它的每个数据节点中都有两个指针,分别指向前一个节点和后一个节点。 所以,从双向链表中的任意一个节点开始,都可以很方便地找到所有节点。
图9-8 LinkedList的存储结构图
每个节点都应该有3部分内容:
1
2
3
4
5 class Node {
Node previous; //前一个节点
Object element; //本节点保存的数据
Node next; //后一个节点
}
我们查看LinkedList的源码,可以看到里面包含了双向链表的相关代码:
图9-9 LinkedList的底层源码
注意事项
entry在英文中表示“进入、词条、条目”的意思。在计算机英语中一般表示“项、条目”的含义。
9.3.4 Vector向量
Vector底层是用数组实现的List,相关的方法都加了同步检查,因此“线程安全,效率低”。 比如,indexOf方法就增加了synchronized同步标记。
图9-10 Vector的底层源码
老鸟建议
如何选用ArrayList、LinkedList、Vector?
1. 需要线程安全时,用Vector。
2. 不存在线程安全问题时,并且查找较多用ArrayList(一般使用它)。
3. 不存在线程安全问题时,增加或删除元素较多用LinkedList。
9.4 Map接口
现实生活中,我们经常需要成对存储某些信息。比如,我们使用的微信,一个手机号只能对应一个微信账户。这就是一种成对存储的关系。
Map就是用来存储“键(key)-值(value) 对”的。 Map类中存储的“键值对”通过键来标识,所以“键对象”不能重复。
Map 接口的实现类有HashMap、TreeMap、HashTable、Properties等。
表9-3 Map接口中常用的方法
9.4.1 HashMap和HashTable
HashMap采用哈希算法实现,是Map接口最常用的实现类。 由于底层采用了哈希表存储数据,我们要求键不能重复,如果发生重复,新的键值对会替换旧的键值对。 HashMap在查找、删除、修改方面都有非常高的效率。
【示例9-7】Map接口中的常用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 public class TestMap {
public static void main(String[] args) {
Map<Integer, String> m1 = new HashMap<Integer, String>();
Map<Integer, String> m2 = new HashMap<Integer, String>();
m1.put(1, “one”);
m1.put(2, “two”);
m1.put(3, “three”);
m2.put(1, “一”);
m2.put(2, “二”);
System.out.println(m1.size());
System.out.println(m1.containsKey(1));
System.out.println(m2.containsValue(“two”));
m1.put(3, “third”); //键重复了,则会替换旧的键值对
Map<Integer, String> m3 = new HashMap<Integer, String>();
m3.putAll(m1);
m3.putAll(m2);
System.out.println(“m1:” + m1);
System.out.println(“m2:” + m2);
System.out.println(“m3:” + m3);
}
}
执行结果如图9-11所示:
图9-11 示例9-7运行效果图
HashTable类和HashMap用法几乎一样,底层实现几乎一样,只不过HashTable的方法添加了synchronized关键字确保线程同步检查,效率较低。
HashMap与HashTable的区别
1. HashMap: 线程不安全,效率高。允许key或value为null。
2. HashTable: 线程安全,效率低。不允许key或value为null。
9.4.2 HashMap底层实现详解
HashMap底层实现采用了哈希表,这是一种非常重要的数据结构。对于我们以后理解很多技术都非常有帮助(比如:redis数据库的核心技术和HashMap一样),因此,非常有必要让大家理解。
数据结构中由数组和链表来实现对数据的存储,他们各有特点。
(1) 数组:占用空间连续。 寻址容易,查询速度快。但是,增加和删除效率非常低。
(2) 链表:占用空间不连续。 寻址困难,查询速度慢。但是,增加和删除效率非常高。
那么,我们能不能结合数组和链表的优点(即查询快,增删效率也高)呢? 答案就是“哈希表”。 哈希表的本质就是“数组+链表”。
老鸟建议
对于本章中频繁出现的“底层实现”讲解,建议学有余力的童鞋将它搞通。刚入门的童鞋如果觉得有难度,可以暂时跳过。入门期间,掌握如何使用即可,底层原理是扎实内功,便于大家应对一些大型企业的笔试面试。
▪ Hashmap基本结构讲解
哈希表的基本结构就是“数组+链表”。我们打开HashMap源码,发现有如下两个核心内容:
图9-12 HashMap底层源码(1)
其中的Entry[] table 就是HashMap的核心数组结构,我们也称之为“位桶数组”。我们再继续看Entry是什么,源码如下:
图9-13 HashMap底层源码(2)
一个Entry对象存储了:
1. key:键对象 value:值对象
2. next:下一个节点
3. hash: 键对象的hash值
显然每一个Entry对象就是一个单向链表结构,我们使用图形表示一个Entry对象的典型示意:
图9-14 Entry对象存储结构图
然后,我们画出Entry[]数组的结构(这也是HashMap的结构):
图9-15 Entry数组存储结构图
▪ 存储数据过程put(key,value)
明白了HashMap的基本结构后,我们继续深入学习HashMap如何存储数据。此处的核心是如何产生hash值,该值用来对应数组的存储位置。
图9-16 HashMap存储数据过程示意图
我们的目的是将”key-value两个对象”成对存放到HashMap的Entry[]数组中。参见以下步骤:
(1) 获得key对象的hashcode
首先调用key对象的hashcode()方法,获得hashcode。
(2) 根据hashcode计算出hash值(要求在[0, 数组长度-1]区间)
hashcode是一个整数,我们需要将它转化成[0, 数组长度-1]的范围。我们要求转化后的hash值尽量均匀地分布在[0,数组长度-1]这个区间,减少“hash冲突”
i. 一种极端简单和低下的算法是:
hash值 = hashcode/hashcode;
也就是说,hash值总是1。意味着,键值对对象都会存储到数组索引1位置,这样就形成一个非常长的链表。相当于每存储一个对象都会发生“hash冲突”,HashMap也退化成了一个“链表”。
ii. 一种简单和常用的算法是(相除取余算法):
hash值 = hashcode%数组长度
这种算法可以让hash值均匀的分布在[0,数组长度-1]的区间。 早期的HashTable就是采用这种算法。但是,这种算法由于使用了“除法”,效率低下。JDK后来改进了算法。首先约定数组长度必须为2的整数幂,这样采用位运算即可实现取余的效果:hash值 = hashcode&(数组长度-1)。
iii. 如下为我们自己测试简单的hash算法:
【示例9-8】测试hash算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 public class Test {
public static void main(String[] args) {
int h = 25860399;
int length = 16;//length为2的整数次幂,则h&(length-1)相当于对length取模
myHash(h, length);
}
/**
* @param h 任意整数
* @param length 长度必须为2的整数幂
* @return
*/
public static int myHash(int h,int length){
System.out.println(h&(length-1));
//length为2的整数幂情况下,和取余的值一样
System.out.println(h%length);//取余数
return h&(length-1);
}
}
运行如上程序,我们就能发现直接取余(h%length)和位运算(h&(length-1))结果是一致的。事实上,为了获得更好的散列效果,JDK对hashcode进行了两次散列处理(核心目标就是为了分布更散更均匀),源码如下:
图9-17 hash算法源码
(3) 生成Entry对象
如上所述,一个Entry对象包含4部分:key对象、value对象、hash值、指向下一个Entry对象的引用。我们现在算出了hash值。下一个Entry对象的引用为null。
(4) 将Entry对象放到table数组中
如果本Entry对象对应的数组索引位置还没有放Entry对象,则直接将Entry对象存储进数组;如果对应索引位置已经有Entry对象,则将已有Entry对象的next指向本Entry对象,形成链表。
总结如上过程:
当添加一个元素(key-value)时,首先计算key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,就形成了链表,同一个链表上的Hash值是相同的,所以说数组存放的是链表。 JDK8中,当链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。
▪ 取数据过程get(key)
我们需要通过key对象获得“键值对”对象,进而返回value对象。明白了存储数据过程,取数据就比较简单了,参见以下步骤:
(1) 获得key的hashcode,通过hash()散列算法得到hash值,进而定位到数组的位置。
(2) 在链表上挨个比较key对象。 调用equals()方法,将key对象和链表上所有节点的key对象进行比较,直到碰到返回true的节点对象为止。
(3) 返回equals()为true的节点对象的value对象。
明白了存取数据的过程,我们再来看一下hashcode()和equals方法的关系:
Java中规定,两个内容相同(equals()为true)的对象必须具有相等的hashCode。因为如果equals()为true而两个对象的hashcode不同;那在整个存储过程中就发生了悖论。
▪ 扩容问题
HashMap的位桶数组,初始大小为16。实际使用时,显然大小是可变的。如果位桶数组中的元素达到(0.75*数组 length), 就重新调整数组大小变为原来2倍大小。
扩容很耗时。扩容的本质是定义新的更大的数组,并将旧数组内容挨个拷贝到新数组中。
▪ JDK8将链表在大于8情况下变为红黑二叉树
JDK8中,HashMap在存储一个元素时,当对应链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。
9.4.3 二叉树和红黑二叉树
二叉树的定义
二叉树是树形结构的一个重要类型。 许多实际问题抽象出来的数据结构往往是二叉树的形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。
二叉树(BinaryTree)由一个节点及两棵互不相交的、分别称作这个根的左子树和右子树的二叉树组成。下图中展现了五种不同基本形态的二叉树。
图9-18 二叉树五种基本形态示意图
(a) 为空树。
(b) 为仅有一个结点的二叉树。
© 是仅有左子树而右子树为空的二叉树。
(d) 是仅有右子树而左子树为空的二叉树。
(e) 是左、右子树均非空的二叉树。
注意事项
二叉树的左子树和右子树是严格区分并且不能随意颠倒的,图 © 与图 (d) 就是两棵不同的二叉树。
排序二叉树特性如下:
(1) 左子树上所有节点的值均小于它的根节点的值。
(2) 右子树上所有节点的值均大于它的根节点的值。
比如:我们要将数据【14,12,23,4,16,13, 8,3】存储到排序二叉树中,如下图所示:
图9-19 排序二叉树示意图(1)
排序二叉树本身实现了排序功能,可以快速检索。但如果插入的节点集本身就是有序的,要么是由小到大排列,要么是由大到小排列,那么最后得到的排序二叉树将变成普通的链表,其检索效率就会很差。 比如上面的数据【14,12,23,4,16,13, 8,3】,我们先进行排序变成:【3,4,8,12,13,14,16,23】,然后存储到排序二叉树中,显然就变成了链表,如下图所示:
图9-20 排序二叉树示意图(2)
▪ 平衡二叉树(AVL)
为了避免出现上述一边倒的存储,科学家提出了“平衡二叉树”。
在平衡二叉树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。 增加和删除节点可能需要通过一次或多次树旋转来重新平衡这个树。
节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子 -2或2的节点被认为是不平衡的,并需要重新平衡这个树。
比如,我们存储排好序的数据【3,4,8,12,13,14,16,23】,增加节点如果出现不平衡,则通过节点的左旋或右旋,重新平衡树结构,最终平衡二叉树如下图所示:
图9-21 平衡二叉树示意图
平衡二叉树追求绝对平衡,实现起来比较麻烦,每次插入新节点需要做的旋转操作次数不能预知。
▪ 红黑二叉树
红黑二叉树(简称:红黑树),它首先是一棵二叉树,同时也是一棵自平衡的排序二叉树。
红黑树在原有的排序二叉树增加了如下几个要求:
1. 每个节点要么是红色,要么是黑色。
2. 根节点永远是黑色的。
3. 所有的叶节点都是空节点(即 null),并且是黑色的。
4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)
5. 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。
这些约束强化了红黑树的关键性质:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。这样就让树大致上是平衡的。
红黑树是一个更高效的检索二叉树,JDK 提供的集合类 TreeMap、TreeSet 本身就是一个红黑树的实现。
图9-22一个典型的红黑树(考虑书本印刷问题,浅色表示红色,深色表示黑色)
红黑树的基本操作:插入、删除、左旋、右旋、着色。 每插入或者删除一个节点,可能会导致树不在符合红黑树的特征,需要进行修复,进行 “左旋、右旋、着色”操作,使树继续保持红黑树的特性。
老鸟建议
本节关于二叉树的介绍,仅限于了解。实际开发中,直接用到的概率非常低。普通企业面试中也较少。不过,极有可能出现在BAT等企业笔试中。建议,想进BAT等名企的童鞋,专门准备一下数据结构相关的知识。
大家理解TreeMap的底层结构。
9.4.4 TreeMap的使用和底层实现
TreeMap是红黑二叉树的典型实现。我们打开TreeMap的源码,发现里面有一行核心代码:
1 private transient Entry<K,V> root = null;
root用来存储整个树的根节点。我们继续跟踪Entry(是TreeMap的内部类)的代码:
图9-23 Entry底层源码
可以看到里面存储了本身数据、左节点、右节点、父节点、以及节点颜色。 TreeMap的put()/remove()方法大量使用了红黑树的理论。本书限于篇幅,不再展开。需要了解更深入的,可以参考专门的数据结构书籍。
TreeMap和HashMap实现了同样的接口Map,因此,用法对于调用者来说没有区别。HashMap效率高于TreeMap;在需要排序的Map时才选用TreeMap。
9.5 Set接口
Set接口继承自Collection,Set接口中没有新增方法,方法和Collection保持完全一致。我们在前面通过List学习的方法,在Set中仍然适用。因此,学习Set的使用将没有任何难度。
Set容器特点:无序、不可重复。无序指Set中的元素没有索引,我们只能遍历查找;不可重复指不允许加入重复的元素。更确切地讲,新元素如果和Set中某个元素通过equals()方法对比为true,则不能加入;甚至,Set中也只能放入一个null元素,不能多个。
Set常用的实现类有:HashSet、TreeSet等,我们一般使用HashSet。
9.5.1 HashSet基本使用
大家在做下面练习时,重点体会“Set是无序、不可重复”的核心要点。
【示例9-9】HashSet的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14 public class Test {
public static void main(String[] args) {
Set s = new HashSet();
s.add(“hello”);
s.add(“world”);
System.out.println(s);
s.add(“hello”); //相同的元素不会被加入
System.out.println(s);
s.add(null);
System.out.println(s);
s.add(null);
System.out.println(s);
}
}
执行结果如图9-24所示:
图9-24示例9-9运行效果图
9.5.2 HashSet底层实现
HashSet是采用哈希算法实现,底层实际是用HashMap实现的(HashSet本质就是一个简化版的HashMap),因此,查询效率和增删效率都比较高。我们来看一下HashSet的源码:
图9-25 HashSet底层源码
我们发现里面有个map属性,这就是HashSet的核心秘密。我们再看add()方法,发现增加一个元素说白了就是在map中增加一个键值对,键对象就是这个元素,值对象是名为PRESENT的Object对象。说白了,就是“往set中加入元素,本质就是把这个元素作为key加入到了内部的map中”。
由于map中key都是不可重复的,因此,Set天然具有“不可重复”的特性。
9.5.3 TreeSet的使用和底层实现
TreeSet底层实际是用TreeMap实现的,内部维持了一个简化版的TreeMap,通过key来存储Set的元素。 TreeSet内部需要对存储的元素进行排序,因此,我们对应的类需要实现Comparable接口。这样,才能根据compareTo()方法比较对象之间的大小,才能进行内部排序。
【示例9-10】TreeSet和Comparable接口的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 public class Test {
public static void main(String[] args) {
User u1 = new User(1001, “高淇”, 18);
User u2 = new User(2001, “高希希”, 5);
Set set = new TreeSet();
set.add(u1);
set.add(u2);
}
}
class User implements Comparable {
int id;
String uname;
int age;
public User(int id, String uname, int age) { this.id = id; this.uname = uname; this.age = age; } /** * 返回0 表示 this == obj 返回正数表示 this > obj 返回负数表示 this < obj */ @Override public int compareTo(User o) { if (this.id > o.id) { return 1; } else if (this.id < o.id) { return -1; } else { return 0; } }
}
使用TreeSet要点:
(1) 由于是二叉树,需要对元素做内部排序。 如果要放入TreeSet中的类没有实现Comparable接口,则会抛出异常:java.lang.ClassCastException。
(2) TreeSet中不能放入null元素。
9.6.2 使用Iterator迭代器遍历容器元素(List/Set/Map)
迭代器为我们提供了统一的遍历容器的方式,参见以下示例代码:
【示例9-11】迭代器遍历List
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 public class Test {
public static void main(String[] args) {
List aList = new ArrayList();
for (int i = 0; i < 5; i++) {
aList.add(“a” + i);
}
System.out.println(aList);
for (Iterator iter = aList.iterator(); iter.hasNext()
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。