目录
1、Java入门
1.1 Java发展简史
1.2 Java的核心优势
1.3 Java各版本的含义
1.4 Java的特性和优势
1.5 Java应用程序的运行机制
1.6 JDK、JAR和JVM
1.7 常见编程语言介绍
2、数据类型和运算符
2.1 注释
2.2 标识符
2.3 变量
2.4 基本数据类型
2.5 运算符
2.6 数据类型的转换
2.7 键盘的输入输出
3、控制语句
3.1 选择结构
3.1.1 if单分支结构
3.1.2 if-else双选择结构
3.1.3 if-else if-else多选择结构
3.1.4 switch多选择结构
3.2 循环结构
3.2.1 while循环
3.2.2 do-while循环
3.2.3 for循环
3.2.4 break语句和continue语句
3.3 代码块
3.4 方法
3.5 方法的重载(overload)
4、Java面向对象编程基础
4.1 面向过程和面向对象思想
4.2 对象和类的概念
4.3 第一个类的定义
4.4 面向对象的内存分析
4.5 构造器
4.6 构造方法的重载
4.7 垃圾回收机制(Garbage Collection)
4.7.1 垃圾回收的原理和算法
4.7.2 通用的分代垃圾回收机制
4.8 this关键字
4.9 static关键字
4.10 静态初始化块
4.11 package
导入类import
5、面向对象编程进阶
5.1 继承
5.1.1 继承的实现
5.1.2 继承使用要点
5.2 Object类
5.2.1 Object类的基本特性
5.2.2 toString方法
5.2.3 ==和equals方法
5.3 super关键字
5.3.1 继承树回溯
5.4 封装
5.4.1 封装的作用和含义
5.4.2 封装的实现—使用访问控制符
5.4.3 封装的使用细节
5.5 多态
5.5.1 对象的类型转换
5.6 final关键字
5.7 抽象方法和抽象类
5.8 接口
5.8.1 接口的作用
5.8.2 定义和使用接口
5.8.3 接口的多继承
5.8.4 面向接口编程
5.9 内部类
5.10 String 基础
5.10.1 String类常用的方法
第五章 总结
6、异常机制
6.1 异常(Exception)的概念
6.2 异常的分类
6.2.1 Error错误
6.2.2 Exception异常
6.2.3 RuntimeException运行时异常
6.2.4 CheckedException已检查异常
6.3 捕获异常
6.5 声明异常(thorws子句)
6.6 自定义异常
第六章 总结
7、数组
7.1 数组声明
7.2 数组的初始化
7.3 数组的遍历
7.4 Arrays类
7.5 多维数组
7.6 数组存储表格数据
7.7 冒泡排序算法
7.7.1 冒泡排序的优化算法
7.8 二分查找算法
第七章 总结
8、常用类
8.1 基本数据类型的包装类
8.1.1 包装类的基本知识
8.1.2 包装类的用途
8.1.3 自动装箱和拆箱
8.2 字符串相关类
8.2.1 String类
8.2.2 StringBuffer和StringBuilder
8.2.3 不可变和可变字符序列使用陷阱
8.3 时间处理相关类
8.3.1 Date时间类(java.util.Date)
8.3.2 DateFormat类和SimpleDateFormat类
8.3.3 Calendar日历类
8.4 Math类
8.5 File类
8.5.1 File类的基本用法
8.5.2 递归遍历目录结构和树状展现
8.6 枚举
第八章 总结
9、容器
9.1 泛型Generics
9.1.1 自定义泛型
9.1.2 容器中使用泛型
9.2 Collection接口
9.3 List接口
9.3.1 List特点和常用方法
9.3.2 ArrayList特点和底层实现
9.3.3 LinkedList特点和底层实现
9.3.4 Vector向量
9.4 Map接口
9.4.1 HashMap和HashTable
9.4.2 HashMap底层实现详解
9.4.3 二叉树和红黑二叉树
9.5 Set接口
9.5.1 HashSet基本使用
9.5.2 HashSet底层实现
9.5.3 TreeSet的使用和底层实现
9.6 Interator接口
9.6.1 迭代器介绍
9.6.2 使用Iterator迭代器遍历容器
9.7 遍历集合的方法总结
9.8 Collections工具类
第九章 总结
1991年,James Gosling在SUN公司的工程师小组想要设计这样一种小型计算机语言。该语言主要用于像电视盒这样的消费类电子产品。另外,由于不同的厂商选择不同的CPU和操作系统,因此,要求该语言不能和特定的体系结构绑在一起,要求语言本身是中立的,也就是跨平台的。所以,将这个语言命名为“Green”,类似于绿色软件的意思。后来,改名为Oak,橡树的意思。改名后发现已经有一种语言叫这个名字了,再改名叫Java。Java语言发展到今天经历了一系列的过程:
1991年,SUN公司的Green项目,Oak
1995年,推出Java测试版
1996年,JDK1.0
1997年,JDK1.1
1998年,JDK1.2,大大改进了早期版本缺陷,是一个革命性的版本,更名为Java2。
2004年,J2SE 5.0 (1.5.0) Tiger老虎
2006年,J2SE 6.0 (1.6.0) Mustang野马
2011年,JavaSE7.0 Dolphin海豚
2014年,JavaSE8.0
Java为消费类智能电子产品而设计,但智能家电产品并没有像最初想象的那样拥有大的发展。然而90年代,Internet却进入了爆发式发展阶段,一夜之间,大家都在忙着将自己的计算机连接到网络上。这个时侯,遇到了一个大的问题。人们发现连接到Internet的计算机各式各样,有IBM PC、苹果机、各种服务器等等,不仅硬件CPU不同,操作系统也不同,整个的网络环境非常复杂。这个时候,程序员们希望他们编写的程序能够运行在不同的机器,不同的环境中,这需要一种体系中立的语言(即跨平台)。Java的研发小组忽然发现他们用于小范围的语言也可以适应Internet这个大环境。
跨平台是Java语言的核心优势,赶上最初互联网的发展,并随着互联网的发展而发展,建立了强大的生态体系,目前已经覆盖IT各行业的“第一大语言”,是计算机界的“英语”。
虽然,目前也有很多跨平台的语言,但是已经失去先机,无法和Java强大的生态体系抗衡。Java仍将在未来几十年成为编程语言的主流语言。
JavaSE(Java Standard Edition):标准版,定位在个人计算机上的应用。
这个版本是Java平台的核心,它提供了非常丰富的API来开发一般个人计算机上的应用程序,包括用户界面接口AWT及Swing,网络功能与国际化、图像处理能力以及输入输出支持等。在上世纪90年代末互联网上大放异彩的Applet也属于这个版本。Applet后来为Flash取代,Flash即将被HTML5取代。
JavaEE(Java Enterprise Edition):企业版,定位在服务器端的应用。
JavaEE是JavaSE的扩展,增加了用于服务器开发的类库。如:JDBC是让程序员能直接在Java内使用的SQL的语法来访问数据库内的数据;Servlet能够延伸服务器的功能,通过请求-响应的模式来处理客户端的请求;JSP是一种可以将Java程序代码内嵌在网页内的技术;
JavaME(Java Micro Edition):微型版,定位在消费性电子产品的应用上
JavaME是JavaSE的内伸,包含J2SE的一部分核心类,也有自己的扩展类,增加了适合微小装置的类库:javax.microedition.io.*等。该版本针对资源有限的电子消费产品的需求精简核心类库,并提供了模块化的架构让不同类型产品能够随时增加支持的能力。
1、跨平台/可移植性
这是Java的核心优势。Java在设计时就很注重移植和跨平台性。比如:Java的int永远都是32位。不像C++可能是16,32,可能是根据编译器厂商规定的变化。这样的话程序的移植就会非常麻烦。
2、安全性
Java适合于网络/分布式环境,为了达到这个目标,在安全性方面投入了很大的精力,使Java可以很容易构建防病毒,防篡改的系统。
3、面向对象
面向对象是一种程序设计技术,非常适合大型软件的设计和开发。由于C++为了照顾大量C语言使用者而兼容了C,使得自身仅仅成为了带类的C语言,多少影响了其面向对象的彻底性!Java则是完全的面向对象语言。
4、简单性
Java就是C++语法的简化版,我们也可以将Java称之为“C++-”。跟我念“C加加减”,指的就是将C++的一些内容去掉;比如:头文件,指针运算,结构,联合,操作符重载,虚基类等等。同时,由于语法基于C语言,因此学习起来完全不费力。
5、高性能
Java最初发展阶段,总是被人诟病“性能低”;客观上,高级语言运行效率总是低于低级语言的,这个无法避免。Java语言本身发展中通过虚拟机的优化提升了几十倍运行效率。比如,通过JIT(JUST IN TIME)即时编译技术提高运行效率。 将一些“热点”字节码编译成本地机器码,并将结果缓存起来,在需要的时候重新调用。这样的话,使Java程序的执行效率大大提高,某些代码甚至接待C++的效率。
因此,Java低性能的短腿,已经被完全解决了。业界发展上,我们也看到很多C++应用转到Java开发,很多C++程序员转型为Java程序员。
6、分布式
Java是为Internet的分布式环境设计的,因为它能够处理TCP/IP协议。事实上,通过URL访问一个网络资源和访问本地文件是一样简单的。Java还支持远程方法调用(RMI,Remote Method Invocation),使程序能够通过网络调用方法。
7、多线程
多线程的使用可以带来更好的交互响应和实时行为。 Java多线程的简单性是Java成为主流服务器端开发语言的主要原因之一。
8、健壮性
Java是一种健壮的语言,吸收了C/C++ 语言的优点,但去掉了其影响程序健壮性的部分(如:指针、内存的申请与释放等)。Java程序不可能造成计算机崩溃。即使Java程序也可能有错误。如果出现某种出乎意料之事,程序也不会崩溃,而是把该异常抛出,再通过异常处理机制加以处理。
计算机高级语言的类型主要有编译型和解释型两种,而Java 语言是两种类型的结合。
Java首先利用文本编辑器编写 Java源程序,源文件的后缀名为.java;再利用编译器(javac)将源程序编译成字节码文件,字节码文件的后缀名为.class; 最后利用虚拟机(解释器,java)解释执行。
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。
计算机语言经历了三代:第一代是机器语言,第二代是汇编语言,第三代是高级语言。目前我们常见的编程语言均为第三代高级语言。
C语言
C语言诞生于1972年,可以称之为现代高级语言的鼻祖,由著名的贝尔实验室发明。C语言是人们追求结构化、模块化、高效率的“语言之花”。在底层编程,比如嵌入式、病毒开发等应用,可以替代汇编语言来开发系统程序。在高层应用,也可以开发从操作系统(Unix/Linux/Windows都基于C语言开发)到各种应用软件。
C++语言
作为C语言的扩展,C++是贝尔实验室于80年代推出的。C++是一种混合语言,既可以实现面向对象编程,也可以开发C语言面向过程风格的程序。
C语言让程序员第一次可以通过结构化的理念编写出易于理解的复杂程序; 尽管C语言是一个伟大的语言,但是程序的代码量达到30000行以上时,程序员就不能很好的从总体上把握和控制这个程序。 因此,在80年代初期,很多软件项目都面临无法解决的问题而不能顺利进行。 1979年,贝尔实验室发明了C++。 C++最初的名字叫做“带类的C”,后来才改名叫C++。国内通用叫法:“C加加”,国际通用读法“C plus plus”。
C++语言在科学计算、操作系统、网络通讯、系统开发、引擎开发中仍然被大量使用。
Java语言
由美国SUN公司发明于1995年,是目前业界应用最广泛、使用人数最多的语言,连续多年排名世界第一,可以称之为“计算机语言界的英语”。
Java广泛应用于企业级软件开发、安卓移动开发、大数据云计算等领域,几乎涉及IT所有行业。关于Java的发展历史和特性,将在后面专门介绍。
PHP语言
PHP原始为Personal Home Page的缩写,已经正式更名为 "PHP: Hypertext Preprocessor"。PHP语言,一般用于WEB开发领域;大量的中小型网站以及某些大型网站使用PHP开发。
Object-C和Swift语言
Object-C通常写作Objective-C或者Obj-C或OC,是根据C语言所衍生出来的语言,继承了C语言的特性,是扩充C的面向对象编程语言。OC主要用于苹果软件的开发。
Swift,苹果于2014年WWDC(苹果开发者大会)发布的新开发语言,可与OC共同运行于Mac OS和iOS平台,用于搭建基于苹果平台的应用程序。
JavaScript语言
JavaScript是一种脚本语言,已经被广泛用于Web应用开发;应用范围越来越大,重要性越来越高。目前,流行的H5开发的核心其实就是JavaScript语言。
Python语言
Python发明于1989年,语法结构简单,易学易懂;Python具有丰富和强大的库。它常被昵称为胶水语言,能够把用其他语言制作的各种模块(尤其是C/C++)很轻松地联结在一起。Python广泛应用于:图形处理、科学计算、web编程、多媒体应用、引擎开发;尤其是在未来大热方向机器学习和人工智能上有非常大的潜力。
C#语言
C#是微软公司发布的一种面向对象的、运行于.NET Framework之上的高级程序设计语言。C#在基于windows操作系统的应用开发这一领域在取代C++,占据主导地位。“成也萧何败也萧何”,C#的微软身份,也成为了发展的阻力,在其他IT领域应用较少。
Fortran语言
世界上第一种高级语言, IBM公司在1954年提出的,主要用在需要复杂数学计算的科学和工程领域。现在仍然被广泛使用,尤其是工程领域。Fortran虽然适合编写科学计算方面的程序,但是不适于编写系统程序。
Basic语言
虽然易学,但功能不够强大,应用到大程序的有效性令人怀疑。已经逐步退出历史舞台。
COBOL语言
于1959年提出。主要用于大量精确处理数据的商业领域中,比如金融、银行。今天,仍然有超过一半的商业软件使用COBOL编写。有将近100万人使用COBOL编程。
Pascal语言
Pascal的名称是为了纪念十七世纪法国著名哲学家和数学家Blaise Pascal而来的,它由瑞士Niklaus Wirth教授于六十年代末设计并创立的。Pascal语言语法严谨,层次分明,程序易写,可读性强,是第一个结构化编程语言。由于没有大厂商和政府的支持,只是限于大学教育这一块。
为了方便程序的阅读,Java语言允许程序员在程序中写上一些说明性的文字,用来提高程序的可读性,这些文字性的说明就称为注释。 注释不会出现在字节码文件中,即Java编译器编译时会跳过注释语句。 在Java中根据注释的功能不同,主要分为单行注释、多行注释和文档注释。
单行注释: 使用“//”开头,“//”后面的单行内容均为注释。
多行注释: 以“/*”开头以“*/”结尾,在“/*”和“*/”之间的内容为注释,我们也可以使用多行注释作为行内注释。但是在使用时要注意,多行注释不能嵌套使用。
文档注释: 以“/**”开头以“*/”结尾,注释中包含一些说明性的文字及一些JavaDoc标签(后期写项目时,可以生成项目的API)
标识符是用来给变量、类、方法以及包进行命名的,如Welcome、main、System、age、name、gender等。标识符需要遵守一定的规则:
标识符必须以字母、下划线_、美元符号$开头。
标识符其它部分可以是字母、下划线“_”、美元符“$”和数字的任意组合。
Java 标识符大小写敏感,且长度无限制。
标识符不可以是Java的关键字。
标识符的使用规范
表示类名的标识符:每个单词的首字母大写,如Man, GoodMan;(大驼峰原则)
表示方法和变量的标识符:第一个单词小写,从第二个单词开始首字母大写,我们称之为“驼峰原则”,如eat(), eatFood();(小驼峰原则)
变量本质上就是代表一个”可操作的存储空间”,空间位置是确定的,但是里面放置什么值不确定。我们可通过变量名来访问“对应的存储空间”,从而操纵这个“存储空间”存储的值。
Java是一种强类型语言,每个变量都必须声明其数据类型。变量的数据类型决定了变量占据存储空间的大小。 比如,int a=3; 表示a变量的空间大小为4个字节。
变量作为程序中最基本的存储单元,其要素包括变量名,变量类型和作用域。变量在使用前必须对其声明, 只有在变量声明以后,才能为其分配相应长度的存储空间。
注意事项
每个变量都有类型,类型可以是基本类型,也可以是引用类型。
变量名必须是合法的标识符
变量声明是一条完整的语句,因此每一个声明都必须以分号结束
Java是一种强类型语言,每个变量都必须声明其数据类型。 Java的数据类型可分为两大类:基本数据类型(primitive data type)和引用数据类型(reference data type)。
Java中定义了3类8种基本数据类型
数值型- byte、 short、int、 long、float、 double
字符型- char
布尔型-boolean
计算机的最基本用途之一就是执行数学运算,作为一门计算机语言,Java也提供了一套丰富的运算符来操作变量。
运算符分类
算术运算符 | 二元运算符 | +,-,*,/,% |
一元运算符 | ++,-- | |
赋值运算符 | = | |
扩展运算符 | +=,-=,*=,/= | |
关系运算符 | >,<,>=,<=,==,!= instanceof | |
逻辑运算符 | &&,||,!,^ | |
位运算符 | &,|,^,~ , >>,<<,>>> | |
条件运算符 | ? : | |
字符串连接符 | + |
运算符的优先级
优先级 | 运算符 | 类 | 结合性 |
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
自动类型转换
自动类型转换指的是容量小的数据类型可以自动转换为容量大的数据类型。如图所示,红色的实线表示无数据丢失的自动类型转换,而蓝色虚线表示在转换时可能会有精度的损失。
强制类型转换
强制类型转换,又被称为造型,用于显式的转换一个数值的类型。在有可能丢失信息的情况下进行的转换是通过造型来完成的,但可能造成精度降低或溢出。
语法格式:(type)var
运算符“()”中的type表示将值var想要转换成的目标数据类型。
为了能写出更加复杂的程序,可以让我们的程序和用户可以通过键盘交互,我们先学习一下简单的键盘输入和输出。
【示例2-31】使用Scanner获取键盘输入
import java.util.Scanner;
/**
* 测试获得键盘输入
* @author HusePanghu
*
*/
public class TestScanner {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入名字:");
String name = scanner.nextLine();//到第一个\n换行停止扫描
System.out.println("请输入你的爱好:");
String favor = scanner.next();//到第一个空白符停止扫描
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所示。
本章开始我们需要学习流程控制语句,流程控制语句是用来控制程序中各语句执行顺序的语句,可以把语句组合成能完成一定功能的小逻辑模块。控制语句分为三类:顺序、选择和循环。
“顺序结构”代表“先执行a,再执行b”的逻辑。比如,先找个女朋友,再给女朋友打电话;先订婚,再结婚;
“选择结构”代表“如果…,则…”的逻辑。比如,如果女朋友来电,则迅速接电话;如果看到红灯,则停车;
“循环结构”代表“如果…,则再继续…”的逻辑。比如,如果没打通女朋友电话,则再继续打一次; 如果没找到喜欢的人,则再继续找。
前面两章讲解的程序都是顺序结构,,即按照书写顺序执行每一条语句,这并不是我们的重点,因此本章研究的重点是“选择结构”和“循环结构”。
在还没有知道Java选择结构的时候,我们编写的程序总是从程序入口开始,顺序执行每一条语句直到执行完最后一条语句结束,但是生活中经常需要进行条件判断,根据判断结果决定是否做一件事情,这就需要选择结构。
选择结构用于判断给定的条件,然后根据判断的结果来控制程序的流程。
主要的选择结构有:if选择结构和switch多选择结构。有如下结构:
if单选择结构
if-else双选择结构
if-else if-else多选择结构
switch结构
语法结构:
1 2 3 |
|
if语句对布尔表达式进行一次判定,若判定为真,则执行{}中的语句块,否则跳过该语句块。流程图如图3-1所示。
if单选择结构流程图
语法结构:
1 2 3 4 5 |
|
当布尔表达式为真时,执行语句块1,否则,执行语句块2。也就是else部分。流程图如图所示。
语法结构:
1 2 3 4 5 6 7 8 9 10 |
|
当布尔表达式1为真时,执行语句块1;否则,判断布尔表达式2,当布尔表达式2为真时,执行语句块2;否则,继续判断布尔表达式3······;如果1~n个布尔表达式均判定为假时,则执行语句块n+1,也就是else部分。流程图如图所示。
语法结构:
1 2 3 4 5 6 7 8 9 10 11 |
|
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多选择结构的流程图如图所示。
循环结构分两大类,一类是当型,一类是直到型。
当型:
当布尔表达式条件为true时,反复执行某语句,当布尔表达式的值为false时才停止循环,比如:while与for循环。
直到型:
先执行某语句, 再判断布尔表达式,如果为true,再执行某语句,如此反复,直到布尔表达式条件为false时才停止循环,比如do-while循环。
语法结构:
1 2 3 |
|
在循环刚开始时,会计算一次“布尔表达式”的值,若条件为真,执行循环体。而对于后来每一次额外的循环,都会在开始前重新计算一次。
语句中应有使循环趋向于结束的语句,否则会出现无限循环–––"死"循环。
while循环结构流程图如图所示。
语法结构:
1 2 3 |
|
do-while循环结构会先执行循环体,然后再判断布尔表达式的值,若条件为真,执行循环体,当条件为假时结束循环。do-while循环的循环体至少执行一次。do-while循环结构流程图如图3.15所示。
语法结构:
1 2 3 |
|
for循环语句是支持迭代的一种通用结构,是最有效、最灵活的循环结构。for循环在第一次反复之前要进行初始化,即执行初始表达式;随后,对布尔表达式进行判定,若判定结果为true,则执行循环体,否则,终止循环;最后在每一次反复的时候,进行某种形式的“步进”,即执行迭代因子。
A. 初始化部分设置循环变量的初值
B. 条件判断部分为任意布尔表达式
C. 迭代因子控制循环变量的增减
for循环在执行条件判定后,先执行的循环体部分,再执行步进。
for循环结构的流程图如图所示。
在任何循环语句的主体部分,均可用break控制循环的流程。break用于强行退出循环,不执行循环中剩余的语句。
示例:break语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
continue 语句用在循环语句体中,用于终止某次循环过程,即跳过循环体中尚未执行的语句,接着进行下一次是否执行循环的判定。
注意事项
1. continue用在while,do-while中,continue 语句立刻跳到循环首部,越过了当前循环的其余部分。
2. continue用在for循环中,跳到for循环的迭代因子部分。
示例:把100~150之间不能被3整除的数输出,并且每行输出5个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
语句块(有时叫做复合语句),是用花括号扩起的任意数量的简单Java语句。块确定了局部变量的作用域。块中的程序代码,作为一个整体,是要被一起执行的。块可以被嵌套在另一个块中,但是不能在两个嵌套的块内声明同名的变量。语句块可以使用外部的变量,而外部不能使用语句块中定义的变量,因为语句块中定义的变量作用域只限于语句块。
1 2 3 4 5 6 7 8 9 10 |
|
方法就是一段用来完成特定功能的代码片段,类似于其它语言的函数。
方法用于定义该类或该类的实例的行为特征和功能实现。 方法是类和对象行为特征的抽象。方法很类似于面向过程中的函数。面向过程中,函数是最基本单位,整个程序由一个个函数调用组成。面向对象中,整个程序的基本单位是类,方法是从属于类和对象的。
方法声明格式:
1 2 3 |
|
方法的调用方式:
对象名.方法名(实参列表)
方法的详细说明
1. 形式参数:在方法声明时用于接收外界传入的数据。
2. 实参:调用方法时实际传给方法的数据。
3. 返回值:方法在执行完毕后返还给调用它的环境的数据。
4. 返回值类型:事先约定的返回值的数据类型,如无返回值,必须显示指定为为void。
方法的重载是指一个类中可以定义多个方法名相同,但参数不同的方法。 调用时,会根据不同的参数自动匹配对应的方法。
雷区
重载的方法,实际是完全不同的方法,只是名称相同而已!
构成方法重载的条件:
1.不同的含义:形参类型、形参个数、形参顺序不同
2.只有返回值不同不构成方法的重载
如:
1 |
|
不构成方法重载
3.只有形参的名称不同,不构成方法的重载
如:
1 |
|
不构成方法重载
示例:方法重载
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 |
|
面向过程(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个天使,带翅膀的美女,让大家看,看完以后,即使我不说,大家下一次是不是就都认识天使了。
图 认识天使
但是,看完10个天使后,我们总要总结一下,什么样的东东才算天使?天使是无数的,总有没见过的!所以必须总结抽象,便于认识未知事物!总结的过程就是抽象的过程。小时候,我们学自然数时怎么定义的?像1,2,3,4…这样的数就叫做自然数。 通过抽象,我们发现天使有这样一下特征:
1. 带翅膀(带翅膀不一定是天使,还可能是鸟人)
2. 女孩(天使掉下来脸着地,也是天使!)
3. 善良
4. 头上有光环
那么通过这4个具体的天使,我们进行抽象,抽象出了天使的特征,我们也可以归纳一个天使类。 通过这个过程,类就是对象的抽象。
类可以看做是一个模版,或者图纸,系统根据类的定义来造出对象。我们要造一个汽车,怎么样造?类就是这个图纸,规定了汽车的详细信息,然后根据图纸将汽车造出来。
类:我们叫做class。 对象:我们叫做Object,instance(实例)。以后我们说某个类的对象,某个类的实例。是一样的意思。
总结
1.对象是具体的事物;对象是对类的具体,类是对对象的抽象;
2.类可以看成一类对象的模板,对象可以看成该类的一个具体实例。
3.类是用于描述同一类型的对象的一个抽象概念,类中定义了这一类对象所应具有的共同的属性、方法。
【示例4-1】类的定义方式
1 2 3 4 5 6 7 8 9 |
|
上面的类定义好后,没有任何的其他信息,就跟我们拿到一张张图纸,但是纸上没有任何信息,这是一个空类,没有任何实际意义。所以,我们需要定义类的具体信息。对于一个类来说,一般有三种常见的成员:属性field、方法method、构造器constructor。这三种成员都可以定义零个或多个。
【示例4-2】简单的学生类编写
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
为了让大家对于面向对象编程有更深入的了解,我们要对程序的执行过程中,内存到底发生了什么变化进行剖析,让大家做到“心中有数”,通过更加形象方式理解程序的执行方式。
建议:
本节课是为了让初学者更深入了解程序底层执行情况,为了完整的体现内存分析流程,会有些新的名词,比如:线程、Class对象。大家暂时可以不求甚解的了解,后期学了这两个概念再回头来看我们这篇内存分析,肯定收获会更大。
Java虚拟机的内存可以分为三个区域:栈stack、堆heap、方法区method area。
栈的特点如下:
1. 栈描述的是方法执行的内存模型。每个方法被调用都会创建一个栈帧(存储局部变量、操作数、方法出口等)
2. JVM为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数、局部变量等)
3. 栈属于线程私有,不能实现线程间的共享!
4. 栈的存储特性是“先进后出,后进先出”
5. 栈是由系统自动分配,速度快!栈是一个连续的内存空间!
堆的特点如下:
1. 堆用于存储创建好的对象和数组(数组也是对象)
2. JVM只有一个堆,被所有线程共享
3. 堆是一个不连续的内存空间,分配灵活,速度慢!
方法区(又叫静态区)特点如下:
1. JVM只有一个方法区,被所有线程共享!
2. 方法区实际也是堆,只是用于存储类、常量相关的信息!
3. 用来存放程序中永远是不变或唯一的内容。(类信息【Class对象】、静态变量、字符串常量等)
图示例 内存分配图
构造器也叫构造方法(constructor),用于对象的初始化。构造器是一个创建对象时被自动调用的特殊方法,目的是对象的初始化。构造器的名称应与类的名称一致。Java通过new关键字来调用构造器,从而返回该类的实例,是一种特殊的方法。
声明格式:
1 2 3 |
|
要点:
1. 通过new关键字调用!!
2. 构造器虽然有返回值,但是不能定义返回值类型(返回值的类型肯定是本类),不能在构造器里使用return返回某个值。
3. 如果我们没有定义构造器,则编译器会自动定义一个无参的构造函数。如果已定义则编译器不会自动添加!
4. 构造器的方法名必须和类名一致!
构造方法也是方法,只不过有特殊的作用而已。与普通方法一样,构造方法也可以重载。
【示例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 |
|
雷区:
如果方法构造中形参名与属性名相同时,需要使用this关键字区分属性与形参。如示例4-6所示:
this.id 表示属性id;id表示形参id
Java引入了垃圾回收机制,令C++程序员最头疼的内存管理问题迎刃而解。Java程序员可以将更多的精力放到业务逻辑上而不是内存管理工作上,大大的提高了开发效率。
·内存管理
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 |
|
s1和s2互相引用对方,导致他们引用计数不为0,但是实际已经无用,但无法被识别。
2. 引用可达法(根搜索算法)
程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
分代垃圾回收机制,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。我们将对象分为三种状态:年轻代、年老代、持久代。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)
· 对象创建的过程和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; String name; String pwd; //构造器,无参构造器 public User(){}; public User(int id, String name){ System.out.println("正在初始化,已经创建好的对象:"+ this); this.id = id; this.name = name; } //方法 public void login(){ System.out.println(this.name + ",要登录"); } public static void main(String[] args){ User user = new User(101,"小王"); System.out.println("打印HusePanghu对象:"+user); user.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 |
|
在类中,用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 |
package 第四章_Java面向对象编程基础.static关键字的使用; /* 在类中,静态成员不能调用非静态成员,比如静态方法、属性; 但是非静态成员可以调用静态成员,比如调用静态方法、静态属性; */ public class User { int id; String name; String pwd; static String company; public User(int id, String name, String company){ this.id = id; this.name = name; this.company = company; } public void login(){ System.out.println("登录" + name); System.out.println("公司:" + company);//非静态成员调用静态属性; this.printCompany();//非静态成员调用静态方法; } public static void printCompany(){ // login();静态成员一调用非静态成员,编译立马报错; System.out.println("公司:" + company); } public static void main(String[] args){ User user = new User(1,"HusePanghu","sherby LMT"); user.login(); user.printCompany(); user.company = "小王公司"; user.printCompany(); } } |
运行结果如图4-9所示。
构造方法用于对象的初始化!静态初始化块,用于类的初始化操作!在静态初始化块中不能直接访问非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 18 |
public class User { int id; String name; String pwd; static String company; //静态初始化块 static{ System.out.println("执行类的初始化工作"); company = "HusePanghu"; printCompany(); } static void printCompany(){ System.out.println("公司:" + company); } public static void main(String[] args){ User user = new User(); } } |
执行结果如图4-11所示。
包机制是Java中管理类的重要手段。 开发中,我们会遇到大量同名的类,通过包我们很容易对解决类重名的问题,也可以实现对类的有效管理。 包对于类,相当于文件夹对于文件的作用。
我们通过package实现对类的管理,package的使用有两个要点:
1. 通常是类的第一句非注释性语句。
2. 包名:域名倒着写即可,再加上模块名,便于内部管理类。
【示例4-13】package的命名举例
1 2 3 4 5 |
|
注意事项:
1. 写项目时都要加包,不要使用默认包。
2. com.gao和com.gao.car,这两个包没有包含关系,是两个完全独立的包。只是逻辑上看起来后者是前者的一部分。
3. 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 |
包含一些实用工具类,如定义系统特性、使用与日期日历相关的函数。 |
如果我们要使用其他包的类,需要使用import导入,从而可以在本类中直接通过类名来调用,否则就需要书写类的完整包名和类名。import后,便于编写代码,提高可维护性。
注意要点:
1. Java会默认导入java.lang包下所有的类,因此这些类我们可以直接使用。
2. 如果导入两个同名的类,只能用包名+类名来显示调用相关类:
1 |
|
【示例4-15】导入同名类的处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
本章重点针对面向对象的三大特征:继承、封装、多态进行详细的讲解。另外还包括抽象类、接口、内部类等概念。很多概念对于初学者来说,更多的是先进行语法性质的了解。不要期望,通过本章学习就“搞透面向对象”。本章只是面向对象的起点,后面所有的章节说白了都是对面向对象这一章的应用。
继承(extends)是面向对象编程的三大特征之一,它让人们更容易实现对已有类的扩展,更容易实现对现实世界的建模。
继承让我们更加容易实现类的扩展。 比如,我们定义了人类,再定义Boy类就只需要扩展人类即可。实现了代码的重用,不用再重新发明轮子(don’t reinvent wheels)。
从英文字面意思理解,extends的意思是“扩展”。子类是父类的扩展。现实世界中的继承无处不在。比如:
图5-1 现实世界中的继承
上图中,哺乳动物继承了动物。意味着,动物的特性,哺乳动物都有;在我们编程中,如果新定义一个Student类,发现已经有Person类包含了我们需要的属性和方法,那么Student类只需要继承Person类即可拥有Person类的属性和方法。
instanceof是二元运算符,左边是对象,右边是类;当对象是右面类或子类所创建对象时,返回true;否则,返回false。比如:
【示例5-1】使用extends实现继承和instanceof
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 34 35 36 37 38 |
class Person{ String name; double height; public void rest(){ System.out.println("人类需要休息!"); } } class student extends Person{ String major; public void study(){ System.out.println("major:" + major); } public student(String name, double height, String major){ //直接继承父类的属性并构造对象; this.name = name; this.height = height; //构造自己的属性 this.major = major; } } public class ExtendsTest { //主方法 public static void main(String[] args){ student xiaowang = new student("小王", 168.0, "csgo"); xiaowang.rest(); xiaowang.study(); //instanceof 检测对象是否继承了某一对象; System.out.println("对象’小王‘是否为Person的子类?"+ (xiaowang instanceof Person)); System.out.println("对象’小王‘是否为student的子类?"+ (xiaowang instanceof student)); System.out.println("对象’小王‘是否为student的子类?"+ (xiaowang instanceof java.lang.Object)); } } |
执行结果如图5-2所示:
1.父类也称作超类、基类、派生类等。
2.Java中只有单继承,没有像C++那样的多继承。多继承会引起混乱,使得继承链过于复杂,系统难于维护。
3.Java中类没有多继承,接口有多继承。
4.子类继承父类,可以得到父类的全部属性和方法 (除了父类的构造方法),但不见得可以直接访问(比如,父类私有的属性和方法)。
5.如果定义一个类时,没有调用extends,则它的父类是:java.lang.Object。
前面学习的所有类以及以后要定义的类都是Object类的子类,也都具备Object类的所有特性。因此,我们非常有必要掌握Object类的用法。
Object类是所有Java类的根基类,也就意味着所有的Java对象都拥有Object类的属性和方法。如果在类的声明中未使用extends关键字指明其父类,则默认继承Object类。
【示例5-4】Object类
1 2 3 4 5 6 7 |
|
Object类中定义有public String toString()方法,其返回值是 String 类型。Object类中toString方法的源码为:
1 2 3 |
|
根据如上源码得知,默认会返回“类名+@+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 20 21 22 |
class Person{ String name; int age; @Override public String toString() { return name + ",年龄:" +age; } } public class OverridetoString { public static void main(String[] args){ Person person = new Person(); person.name = "小王"; person.age = 21; System.out.println("Person.toString:" + person); OverridetoString test = new OverridetoString(); System.out.println("OverridetoString:" + test); System.out.println("OverridetoString:" + test.toString()); } } |
执行结果如图所示:
“==”代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表示地址相等即是同一个对象。
Object类中定义有:public boolean equals(Object obj)方法,提供定义“对象内容相等”的逻辑。比如,我们在公安系统中认为id相同的人就是同一个人、学籍系统中认为学号相同的人就是同一个人。
Object 的 equals 方法默认就是比较两个对象的hashcode,是同一个对象的引用时返回 true 否则返回 false。但是,我们可以根据我们自己的要求重写equals方法。
【示例】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 34 35 |
class people{ int id; String name; public people(int id, String name){ this.id = id; this.name = name; } /* * 重写equals() * 需求:在people类中重写,对比连个people对象,只要两个people对象的id相同,就认为相等。*/ @Override public boolean equals(Object obj){ if(obj == null){ return false; }else{ if(obj instanceof people){ people c = (people) obj;//强制类型转换 if(c.id == this.id){ return true; } } } return false; } } public class OverrideEquals { public static void main(String[] args) { people xw = new people(1, "小王"); people xxw = new people(1, "小小王"); System.out.println("==判断:" + (xw == xxw)); System.out.println("people.equals():" + xw.equals(xxw)); } } |
JDK提供的一些类,如String、Date、包装类等,重写了Object的equals方法,调用这些类的equals方法, x.equals (y) ,当x和y所引用的对象是同一类对象且属性内容相等时(并不一定是相同对象),返回 true 否则返回 false。
super是直接父类对象的引用。可以通过super来访问父类中被子类覆盖的方法或属性。
使用super调用普通方法,语句没有位置限制,可以在子类中随便调用。
若是构造方法的第一行代码没有显式的调用super(...)或者this(...);那么Java默认都会调用super(),含义是调用父类的无参数构造方法。这里的super()可以省略。
【示例】super关键字的使用
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 |
/* super:对父类对象的直接引用; super.f(),调用父类中的f()方法;super.value,调用父类中的成员变量value; */ class FatherClass{ public int value; public void f(){ value = 100; System.out.println("FatcherClass.value = " + value); } } class ChildClass extends FatherClass{ public int value; public void f(){ super.f();//调用父类中的f()方法; value = 200; System.out.println("ChildClass.value = " + value); System.out.println("FatherClass.value = " + super.value);//调用父类对象的成员变量; } } public class TestSuper { public static void main(String[] args) { // ChildClass childclass = new ChildClass(); // childclass.f(); new ChildClass().f();//上两行代码的简化; } } |
执行结果如图所示:
·属性/方法查找顺序:(比如:查找变量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 |
|
执行结果如图5-6所示:
封装(encapsulation)是面向对象编程的三大特征之一。对于程序的合理封装可以使外部调用更加方便,更加利于编程。同时,对于实现者来说也更容易修正和改版代码。
我要看电视,只需要按一下开关和换台就可以了。有必要了解电视机内部的结构吗?有必要碰碰显像管吗?制造厂家为了方便我们使用电视,把复杂的内部细节全部封装起来,只给我们暴露简单的接口,比如:电源开关。具体内部是怎么实现的,我们不需要操心。
需要让用户知道的才暴露出来,不需要让用户知道的全部隐藏起来,这就是封装。说的专业一点,封装就是把对象的属性和操作结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。
我们程序设计要追求“高内聚,低耦合”。 高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合是仅暴露少量的方法给外部使用,尽量方便外部调用。
编程中封装的具体优点:
1. 提高代码的安全性。
2. 提高代码的复用性。
3. “高内聚”:封装细节,便于修改内部代码,提高可维护性。
4. “低耦合”:简化外部调用,便于调用者使用,便于扩展和协作。
【示例】没有封装的代码会出现一些问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
我们都知道,年龄不可能是负数,也不可能超过130岁,但是如果没有使用封装的话,便可以给年龄赋值成任意的整数,这显然不符合我们的正常逻辑思维。执行结果如图5-7所示:
示例 运行效果图
再比如说,如果哪天我们需要将Person类中的age属性修改为String类型的,你会怎么办?你只有一处使用了这个类的话那还比较幸运,但如果你有几十处甚至上百处都用到了,那你岂不是要改到崩溃。而封装恰恰能解决这样的问题。如果使用封装,我们只需要稍微修改下Person类的setAge()方法即可,而无需修改使用了该类的客户代码。
Java是使用“访问控制符”来控制哪些细节需要封装,哪些细节需要暴露的。 Java中4种“访问控制符”分别为private、default、protected、public,它们说明了面向对象的封装性,所以我们要利用它们尽可能的让访问权限降到最低,从而提高安全性。
下面详细讲述它们的访问权限问题。其访问权限范围如表5-1所示。
表5-1 访问权限修饰符
1. private 表示私有,只有自己类能访问
2. default表示没有修饰符修饰,只有同一个包的类能访问
3. protected表示可以被同一个包的类以及其他包中的子类访问
4. public表示可以被该项目的所有包中的所有类访问
类的属性的处理:
1. 一般使用private访问权限。
2. 提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的,以提供对属性的赋值与读取操作(注意:boolean变量的get方法是is开头!)。
3. 一些只用于本类的辅助性方法可以用private修饰,希望其他类调用的方法用public修饰。
【示例 】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 |
|
下面我们使用封装来解决一下5.4.1中提到的年龄非法赋值的问题。
【示例 】封装的使用
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 |
|
执行结果如图5-16所示:
多态(polymorphism)指的是同一个方法调用,由于对象不同可能会有不同的行为。现实生活中,同一个方法,具体实现会完全不同。 比如:同样是调用人的“休息”方法,张三是睡觉,李四是旅游,高淇老师是敲代码,数学教授是做数学题; 同样是调用人“吃饭”的方法,中国人用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。
多态的要点:
1. 多态是方法的多态,不是属性的多态(多态与属性无关)。
2. 多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象。
3. 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。
多态首先建立在继承的基础上,先有继承再有多态。多态是指不同子类在继承了父类的方法之后,分别都对父类的方法进行了重写,即一个父类方法在子类中表现出不同的形式。多态成立的另一个条件就是子类的创建必须使用父类new子类的形式,即父类引用指向子类对象。
【示例 】多态和类型转换测试
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 |
|
执行结果如图 所示:
图 示例 运行效果图
示例 给大家展示了多态最为多见的一种用法,即父类引用做方法的形参,实参可以是任意的子类对象,可以通过不同的子类对象实现不同的行为方式。
由此,我们可以看出多态的主要优势是提高了代码的可扩展性,符合开闭原则。但是多态也有弊端,就是无法调用子类特有的功能,比如,我不能使用父类的引用变量调用Dog类特有的seeDoor()方法。
那如果我们就想使用子类特有的功能行不行呢?行!这就是我们下一章节所讲的内容:对象的转型。
父类引用指向子类对象,我们称这个过程为向上转型,属于自动类型转换。
向上转型后的父类引用变量只能调用它编译类型的方法,不能调用它运行时类型的方法。这时,我们就需要进行类型的强制转换,我们称之为向下转型!
【示例 】对象的转型
1 2 3 4 5 6 7 8 9 10 11 |
|
执行结果如果5-18所示:
图 示例 运行效果图
在向下转型过程中,必须将引用变量转成真实的子类类型(运行时类型)否则会出现类型转换异常ClassCastException。如示例 所示。
final关键字的作用:
1. 修饰变量: 被他修饰的变量不可改变。一旦赋了初值,就不能被重新赋值。
1 |
|
2. 修饰方法:该方法不可被子类重写。但是可以被重载!
1 |
|
3. 修饰类: 修饰的类不能被继承。比如:Math、String等。
1 |
|
final修饰方法如图 所示。
图 final修饰方法
final修饰类如图 所示。
·抽象方法
使用abstract修饰的方法,没有方法体,只有声明。定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体的实现。
·抽象类
包含抽象方法的类就是抽象类。通过abstract方法定义规范,然后要求子类必须定义具体实现。通过抽象类,我们就可以做到严格限制子类的设计,使子类之间更加通用。
【示例 】抽象类和抽象方法的基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
抽象类的使用要点:
1. 有抽象方法的类只能定义成抽象类
2. 抽象类不能实例化,即不能用new来实例化抽象类。
3. 抽象类可以包含属性、方法、构造方法。但是构造方法不能用来new实例,只能用来被子类调用。
4. 抽象类只能用来被继承。
5. 抽象方法必须被子类实现。
接口就是规范,定义的是一组规则,体现了现实世界中“如果你是…则必须能…”的思想。如果你是天使,则必须能飞。如果你是汽车,则必须能跑。如果你是好人,则必须能干掉坏人;如果你是坏人,则必须欺负好人。
接口的本质是契约,就像我们人间的法律一样。制定好后大家都遵守。
面向对象的精髓,是对对象的抽象,最能体现这一点的就是接口。为什么我们讨论设计模式都只针对具备了抽象能力的语言(比如C++、Java、C#等),就是因为设计模式所研究的,实际上就是如何合理的去抽象。
为什么需要接口?接口和抽象类的区别是什么?
接口就是比“抽象类”还“抽象”的“抽象类”,可以更加规范的对子类进行约束。全面地专业地实现了:规范和具体实现的分离。
抽象类还提供某些具体实现,接口不提供任何实现,接口中所有方法都是抽象方法。接口是完全面向规范的,规定了一批类具有的公共方法规范。
从接口的实现者角度看,接口定义了可以向外部提供的服务。
从接口的调用者角度看,接口定义了实现者能提供那些服务。
接口是两个模块之间通信的标准,通信的规范。如果能把你要设计的模块之间的接口定义好,就相当于完成了系统的设计大纲,剩下的就是添砖加瓦的具体实现了。大家在工作以后,做系统时往往就是使用“面向接口”的思想来设计系统。
接口和实现类不是父子关系,是实现规则的关系。比如:我定义一个接口Runnable,Car实现它就能在地上跑,Train实现它也能在地上跑,飞机实现它也能在地上跑。就是说,如果它是交通工具,就一定能跑,但是一定要实现Runnable接口。
区别
1. 普通类:具体实现
2. 抽象类:具体实现,规范(抽象方法)
3. 接口:规范!
声明格式:
1 2 3 4 |
|
定义接口的详细说明:
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后,接口中包含普通的静态方法。
【示例 】接口的使用
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 |
|
执行结果如果 所示:
接口完全支持多继承。和类的继承类似,子接口扩展某个父接口,将会获得父接口中所定义的一切。
【示例 】接口的多继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
面向接口编程是面向对象编程的一部分。
为什么需要面向接口编程? 软件设计中最难处理的就是需求的复杂变化,需求的变化更多的体现在具体实现上。我们的编程如果围绕具体实现来展开就会陷入”复杂变化”的汪洋大海中,软件也就不能最终实现。我们必须围绕某种稳定的东西开展,才能以静制动,实现规范的高质量的项目。
接口就是规范,就是项目中最稳定的东东! 面向接口编程可以让我们把握住真正核心的东西,使实现复杂多变的需求成为可能。
通过面向接口编程,而不是面向实现类编程,可以大大降低程序模块间的耦合性,提高整个系统的可扩展性和和可维护性。
面向接口编程的概念比接口本身的概念要大得多。设计阶段相对比较困难,在你没有写实现时就要想好接口,接口一变就乱套了,所以设计要比实现难!
接口语法本身非常简单,但是如何真正使用?这才是大学问。我们需要后面在项目中反复使用,大家才能体会到。 学到此处,能了解基本概念,熟悉基本语法,就是“好学生”了。 请继续努力!再请工作后,闲余时间再看看上面这段话,相信你会有更深的体会。
一般情况,我们把类定义成独立的单元。有些情况下,我们把一个类放在另一个类的内部定义,称为内部类(innerclasses)。
内部类可以使用public、default、protected 、private以及static修饰。而外部顶级类(我们以前接触的类)只能使用public和default修饰。
注意
内部类只是一个编译时概念,一旦我们编译成功,就会成为完全不同的两个类。对于一个名为Outer的外部类和其内部定义的名为Inner的内部类。编译完成后会出现Outer.class和Outer$Inner.class两个类的字节码文件。所以内部类是相对独立的一种存在,其成员变量/方法名可以和外部类的相同。
【示例 】内部类介绍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
示例 编译后会产生两个不同的字节码文件,如图 所示:
图 内部类编译结果图
内部类的作用:
1. 内部类提供了更好的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问。
2. 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。 但外部类不能访问内部类的内部属性。
3. 接口只是解决了多重继承的部分问题,而内部类使得多重继承的解决方案变得更加完整。
内部类的使用场合:
1. 由于内部类提供了更好的封装特性,并且可以很方便的访问外部类的属性。所以,在只为外部类提供服务的情况下可以优先考虑使用内部类。
2. 使用内部类间接实现多继承:每个内部类都能独立地继承一个类或者实现某些接口,所以无论外部类是否已经继承了某个类或者实现了某些接口,对于内部类没有任何影响。
1. String类又称作不可变字符序列。
2. String位于java.lang包中,Java程序默认导入java.lang包下的所有类。
3. Java字符串就是Unicode字符序列,例如字符串“Java”就是4个Unicode字符’J’、’a’、’v’、’a’组成的。
4. Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义的类String,每个用双引号括起来的字符串都是String类的一个实例。
【示例 】String类的实例
1 2 |
|
5. Java允许使用符号"+"把两个字符串连接起来。
【示例 】字符串连接
1 2 3 |
|
n-符号"+"把两个字符串按给定的顺序连接在一起,并且是完全按照给定的形式。
n-当"+"运算符两侧的操作数中只要有一个是字符串(String)类型,系统会自动将另一个操作数转换为字符串然后再进行连接。
【示例 】"+"连接符
1 2 3 4 |
|
String类是我们最常使用的类。字符串类的方法我们必须非常熟悉!我们列出常用的方法,请大家熟悉。
表 String类的常用方法列表
【示例 】String类常用方法一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
执行结果如图 所示:
图 示例 运行效果图
【示例 】String类常用方法二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
执行结果如图5-32所示:
· 高级语言可分为:面向过程和面向对象两大类
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()方法的区别。
在实际工作中,我们遇到的情况不可能是非常完美的。比如:你写的某个模块,用户输入不一定符合你的要求;你的程序要打开某个文件,这个文件可能不存在或者文件格式不对;你要读取数据库的数据,数据可能是空的;我们的程序再运行着,但是内存或硬盘可能满了等等。
软件程序在运行过程中,非常可能遇到刚刚提到的这些问题,我们称之为异常,英文是:Exception,意思是例外。遇到这些例外情况,或者叫异常,我们怎么让写的程序做出合理的处理,安全的退出,而不至于程序崩溃呢?我们本章就要讲解这些问题。
异常指程序运行过程中出现的非正常现象,例如用户输入错误、除数为零、需要处理的文件不存在、数组下标越界等。
在Java的异常处理机制中,引进了很多用来描述和处理异常的类,称为异常类。异常类定义中包含了该类异常的信息和对异常进行处理的方法。
所谓异常处理,就是指程序在出现问题时依然可以正确的执行完。
我们开始看我们的第一个异常对象,并分析一下异常机制是如何工作的。
【示例 】异常的分析
1 2 3 4 5 6 |
|
执行结果如图6-1所示:
图 示例 运行效果图
Java是采用面向对象的方式来处理异常的。处理过程:
1. 抛出异常:在执行一个方法时,如果发生异常,则这个方法生成代表该异常的一个对象,停止当前执行路径,并把异常对象提交给JRE。
2. 捕获异常:JRE得到该异常后,寻找相应的代码来处理该异常。JRE在方法的调用栈中查找,从生成异常的方法开始回溯,直到找到相应的异常处理代码为止。
JDK 中定义了很多异常类,这些类对应了各种各样可能出现的异常事件,所有异常对象都是派生于Throwable类的一个实例。如果内置的异常类不能够满足需要,还可以创建自己的异常类。
Java对异常进行了分类,不同类型的异常分别用不同的Java类表示,所有异常的根类为java.lang.Throwable,Throwable下面又派生了两个子类:Error和Exception。Java异常类的层次结构如图6-2所示。
Error是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
Error表明系统JVM已经处于不可恢复的崩溃状态中。我们不需要管它。
图 java.lang包中Error的类
Error与Exception的区别
1. 我开着车走在路上,一头猪冲在路中间,我刹车。这叫一个异常。
2. 我开着车在路上,发动机坏了,我停车,这叫错误。系统处于不可恢复的崩溃状态。发动机什么时候坏?我们普通司机能管吗?不能。发动机什么时候坏是汽车厂发动机制造商的事。
Exception是程序本身能够处理的异常,如:空指针异常(NullPointerException)、数组下标越界异常(ArrayIndexOutOfBoundsException)、类型转换异常(ClassCastException)、算术异常(ArithmeticException)等。
Exception类是所有异常类的父类,其子类对应了各种各样可能出现的异常事件。 通常Java的异常可分为:
1. RuntimeException 运行时异常
2. CheckedException 已检查异常
派生于RuntimeException的异常,如被 0 除、数组下标越界、空指针等,其产生比较频繁,处理麻烦,如果显式的声明或捕获将会对程序可读性和运行效率影响很大。 因此由系统自动检测并将它们交给缺省的异常处理程序(用户可不必对其处理)。
这类异常通常是由编程错误导致的,所以在编写程序时,并不要求必须使用异常处理机制来处理这类异常,经常需要通过增加“逻辑处理来避免这些异常”。
【示例 】ArithmeticException异常:试图除以0
1 2 3 4 5 6 |
|
执行结果如图 所示:
图 ArithmeticException异常
解决如上异常需要修改代码:
1 2 3 4 5 6 7 8 |
|
当程序访问一个空对象的成员变量或方法,或者访问一个空数组的成员时会发生空指针异常(NullPointerException)。怎么处理?
【示例 】NullPointerException异常
1 2 3 4 5 6 |
|
执行结果如图 所示:
图 NullPointerException异常
解决空指针异常,通常是增加非空判断:
1 2 3 4 5 6 7 8 |
|
在引用数据类型转换时,有可能发生类型转换异常(ClassCastException)。
【示例 】ClassCastException异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
执行结果如图 所示:
图 ClassCastException异常
解决ClassCastException的典型方式:
1 2 3 4 5 6 7 8 |
|
当程序访问一个数组的某个元素时,如果这个元素的索引超出了0~数组长度-1这个范围,则会出现数组下标越界异常(ArrayIndexOutOfBoundsException)。
【示例 】ArrayIndexOutOfBoundsException异常
1 2 3 4 5 6 |
|
执行结果如图 所示:
图 ArrayIndexOutOfBoundsException异常
解决数组索引越界异常的方式,增加关于边界的判断:
1 2 3 4 5 6 7 8 9 |
|
在使用包装类将字符串转换成基本数据类型时,如果字符串的格式不正确,则会出现数字格式异常(NumberFormatException)。
【示例 】NumberFormatException异常
1 2 3 4 5 6 |
|
执行结果如图 所示:
图 NumberFormatException异常
数字格式化异常的解决,可以引入正则表达式判断是否为数字:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
注意事项
1. 在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。
2. 运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。
所有不是RuntimeException的异常,统称为Checked Exception,又被称为“已检查异常”,如IOException、SQLException等以及用户自定义的Exception异常。 这类异常在编译时就必须做出处理,否则无法通过编译。如图所示。
图 CheckedException异常
如图所示,异常的处理方式有两种:使用“try/catch”捕获异常、使用“throws”声明异常。
捕获异常是通过3个关键词来实现的:try-catch-finally。用try来执行一段程序,如果出现异常,系统抛出一个异常,可以通过它的类型来捕捉(catch)并处理它,最后一步是通过finally语句为异常处理提供一个统一的出口,finally所指定的代码都要被执行(catch语句可有多条;finally语句最多只能有一条,根据自己的需要可有可无)。如图6-10所示。
图 异常处理
上面过程详细解析:
1. try:
try语句指定了一段代码,该段代码就是异常捕获并处理的范围。在执行过程中,当任意一条语句产生异常时,就会跳过该条语句中后面的代码。代码中可能会产生并抛出一种或几种类型的异常对象,它后面的catch语句要分别对这些异常做相应的处理。
一个try语句必须带有至少一个catch语句块或一个finally语句块 。
注意事项
当异常处理的代码执行结束以后,不会回到try语句去执行尚未执行的代码。
2. catch:
n-每个try语句块可以伴随一个或多个catch语句,用于处理可能产生的不同类型的异常对象。
n-常用方法,这些方法均继承自Throwable类 。
u-toString ()方法,显示异常的类名和产生异常的原因
u-getMessage()方法,只显示产生异常的原因,但不显示类名。
u-printStackTrace()方法,用来跟踪异常事件发生时堆栈的内容。
n-catch捕获异常时的捕获顺序:
u-如果异常类之间有继承关系,在顺序安排上需注意。越是顶层的类,越放在下面,再不然就直接把多余的catch省略掉。 也就是先捕获子类异常再捕获父类异常。
2. finally:
n-有些语句,不管是否发生了异常,都必须要执行,那么就可以把这样的语句放到finally语句块中。
n-通常在finally中关闭程序块已打开的资源,比如:关闭文件流、释放数据库连接等。
try-catch-finally语句块的执行过程:
try-catch-finally程序块的执行流程以及执行结果比较复杂。
基本执行过程如下:
程序首先执行可能发生异常的try语句块。如果try语句没有出现异常则执行完后跳至finally语句块执行;如果try语句出现异常,则中断执行并根据发生的异常类型跳至相应的catch语句块执行处理。catch语句块可以有多个,分别捕获不同类型的异常。catch语句块执行完后程序会继续执行finally语句块。finally语句是可选的,如果有的话,则不管是否发生异常,finally语句都会被执行。
注意事项
1. 即使try和catch块中存在return语句,finally语句也会执行。是在执行完finally语句后再通过return退出。
2. finally语句块只有一种情况是不会执行的,那就是在执行finally之前遇到了System.exit(0)结束程序运行。
【示例 】典型代码
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 |
|
当CheckedException产生时,不一定立刻处理它,可以再把异常throws出去。
在方法中使用try-catch-finally是由这个方法来处理异常。但是在一些情况下,当前方法并不需要处理发生的异常,而是向上传递给调用它的方法处理。
如果一个方法中可能产生某种异常,但是并不能确定如何处理这种异常,则应根据异常规范在方法的首部声明该方法可能抛出的异常。
如果一个方法抛出多个已检查异常,就必须在方法的首部列出所有的异常,之间以逗号隔开。
【示例 】典型代码
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 |
|
注意事项
1. 方法重写中声明异常原则:子类重写父类方法时,如果父类方法有声明异常,那么子类声明的异常范围不能超过父类声明的范围。
1.在程序中,可能会遇到JDK提供的任何标准异常类都无法充分描述清楚我们想要表达的问题,这种情况下可以创建自己的异常类,即自定义异常类。
2.自定义异常类只需从Exception类或者它的子类派生一个子类即可。
3.自定义异常类如果继承Exception类,则为受检查异常,必须对其进行处理;如果不想处理,可以让自定义异常类继承运行时异常RuntimeException类。
4.习惯上,自定义异常类应该包含2个构造器:一个是默认的构造器,另一个是带有详细信息的构造器。
【示例 】自定义异常类
1 2 3 4 5 6 7 8 9 10 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 |
|
执行结果如图 所示:
图 示例 运行效果图
使用异常机制的建议
1.要避免使用异常处理代替错误处理,这样会降低程序的清晰性,并且效率低下。
2.处理异常不可以代替简单测试---只在异常情况下使用异常机制。
3.不要进行小粒度的异常处理---应该将整个任务包装在一个try语句块中。
4.异常往往在高层处理(先了解!后面做项目会说!) 。
1. Error与Exception都继承自Throwable类
2. Error类层次描述了Java运行时系统内部错误和资源耗尽错误。
3. Exception类是所有异常类的父类,其子类对应了各种各样可能出现的异常事件。
4. 常见的异常类型
--ArithmeticException
--NullPointerException
--ClassCastException
--ArrayIndexOutOfBoundsException
--NumberFormatException
5. 方法重写中声明异常原则:子类声明的异常范围不能超过父类声明的范围
6. 异常处理的三种方式
--捕获异常:try-catch-finally
--声明异常:throws
7. 自定义异常类只需从Exception类或者它的子类派生一个子类即可。
数组的定义
数组是相同类型数据的有序集合。数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成。其中,每一个数据称作一个元素,每个元素可以通过一个索引(下标)来访问它们。数组的三个基本特点:
1. 长度是确定的。数组一旦被创建,它的大小就是不可以改变的。
2. 其元素必须是相同类型,不允许出现混合类型。
3. 数组类型可以是任何数据类型,包括基本类型和引用类型。
建议
数组变量属引用类型,数组也可以看成是对象,数组中的每个元素相当于该对象的成员变量。数组本身就是对象,Java中对象是在堆中的,因此数组无论保存原始类型还是其他对象类型,数组对象本身是在堆中存储的。
数组的声明方式有两种(以一维数组为例)
1 2 |
|
注意事项
1. 声明的时候并没有实例化任何对象,只有在实例化数组对象时,JVM才分配空间,这时才与长度有关。
2. 声明一个数组的时候并没有数组真正被创建。
3. 构造一个数组,必须指定长度。
【示例 】创建基本类型一维数组
1 2 3 4 5 6 7 8 9 10 |
|
图 基本类型数组内存分配图
【示例 】创建引用类型一维数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
数组的初始化方式总共有三种:静态初始化、动态初始化、默认初始化。下面针对这三种方式分别讲解。
1. 静态初始化
除了用new关键字来产生数组以外,还可以直接在定义数组的同时就为数组元素分配空间并赋值。
【示例 】静态初始化数组
1 2 |
|
2.动态初始化
数组定义与为数组元素分配空间并赋值的操作分开进行。
【示例 】动态初始化数组
1 2 3 |
|
3.数组的默认初始化
数组是引用类型,它的元素相当于类的实例变量,因此数组一经分配空间,其中的每个元素也被按照实例变量同样的方式被隐式初始化。
【示例 】数组的默认初始化
1 2 3 |
|
数组元素下标的合法区间:[0, length-1]。我们可以通过下标来遍历数组中的元素,遍历时可以读取元素的值或者修改元素的值。
【示例 】 使用循环遍历初始化和读取数组
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
执行结果如图所示:
增强for循环for-each是JDK1.5新增加的功能,专门用于读取数组或集合中所有的元素,即对数组进行遍历。
【示例 】增强for循环
1 2 3 4 5 6 7 8 |
|
执行结果如图7-4所示:
图 示例 运行效果图
注意事项
1. for-each增强for循环在遍历数组过程中不能修改数组中某元素的值。
2. for-each仅适用于遍历,不涉及有关索引(下标)的操作。
数组的拷贝
System类里也包含了一个static void arraycopy(object src,int srcpos,object dest, int destpos,int length)方法,该方法可以将src数组里的元素值赋给dest数组的元素,其中srcpos指定从src数组的第几个元素开始赋值,length参数指定将src数组的多少个元素赋给dest数组的元素。
【示例】数组拷贝
1 2 3 4 5 6 7 8 9 10 |
|
执行结果如图所示:
JDK提供的java.util.Arrays类,包含了常用的数组操作,方便我们日常开发。Arrays类包含了:排序、查找、填充、打印内容等常见的操作。
【示例 】打印数组
1 2 3 4 5 6 7 8 |
|
执行结果如图 所示:
图 示例 运行效果图
菜鸟雷区
此处的Arrays.toString()方法是Arrays类的静态方法,不是前面讲的Object的toString()方法。
【示例 】数组元素的排序
1 2 3 4 5 6 7 8 9 |
|
执行结果如图 所示:
图 示例 运行效果图
【示例 】数组元素是引用类型的排序(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 |
|
【示例 】二分法查找
1 2 3 4 5 6 7 8 9 10 11 |
|
执行结果如图 所示:
图 示例 运行效果图
【示例 】数组填充
1 2 3 4 5 6 7 8 9 |
|
执行结果如图7-9所示:
多维数组可以看成以数组为元素的数组。可以有二维、三维、甚至更多维数组,但是实际开发中用的非常少。最多到二维数组(学习容器后,我们一般使用容器,二维数组用的都很少)。
【示例 】二维数组的声明
1 2 3 4 5 6 7 8 9 10 |
|
【示例 】二维数组的静态初始化
1 2 3 4 5 6 |
|
图 示例 内存分配图
【示例 】二维数组的动态初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
执行结果如图 所示:
图 示例 运行效果图
【示例 】获取数组长度
1 2 3 4 |
|
表格数据模型是计算机世界最普遍的模型,可以这么说,大家在互联网上看到的所有数据本质上都是“表格”,无非是表格之间互相套用。如下表格是一张雇员表:
表 雇员表
我们观察表格,发现每一行可以使用一个一维数组存储:
1 2 3 |
|
注意事项
此处基本数据类型”1001”,本质不是Object对象。JAVA编译器会自动把基本数据类型“自动装箱”成包装类对象。大家在下一章学了包装类后就懂了。
这样我们只需要再定义一个二维数组,将上面3个数组放入即可:
1 2 3 4 |
|
【示例 】 二维数组保存表格数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
执行结果如图 所示:
冒泡排序是最常用的排序算法,在笔试中也非常常见,能手写出冒泡排序算法可以说是基本的素养。
算法重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来,这样越大的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序算法的运作如下:
1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
3. 针对所有的元素重复以上的步骤,除了最后一个。
4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
大家可以用如上思想,将下面的人按照身高从低到高重新排列:
图 身高图
【示例 】冒泡排序的基础算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
执行结果如图 所示:
其实,我们可以把之前的冒泡排序的算法优化一下,基于冒泡排序的以下特点:
1.整个数列分成两部分:前面是无序数列,后面是有序数列。
2.初始状态下,整个数列都是无序的,有序数列是空。
3.每一趟循环可以让无序数列中最大数排到最后,(也就是说有序数列的元素个数增加1),也就是不用再去顾及有序序列。
4.每一趟循环都从数列的第一个元素开始进行比较,依次比较相邻的两个元素,比较到无序数列的末尾即可(而不是数列的末尾);如果前一个大于后一个,交换。
5.判断每一趟是否发生了数组元素的交换,如果没有发生,则说明此时数组已经有序,无需再进行后续趟数的比较了。此时可以中止比较。
【示例 】冒泡排序的优化算法
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 |
|
执行结果如图 所示:
二分法检索(binary search)又称折半检索,二分法检索的基本思想是设数组中的元素从小到大有序地存放在数组(array)中,首先将给定值key与数组中间位置上元素的关键码(key)比较,如果相等,则检索成功;
否则,若key小,则在数组前半部分中继续进行二分法检索;
若key大,则在数组后半部分中继续进行二分法检索。
这样,经过一次比较就缩小一半的检索区间,如此进行下去,直到检索成功或检索失败。
二分法检索是一种效率较高的检索方法。比如,我们要在数组[7, 8, 9, 10, 12, 20, 30, 40, 50, 80, 100]中查询到10元素,过程如下:
图 二分法示意图
【示例 】二分法查找
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 |
|
执行结果如图 所示:
1. 数组是相同类型数据的有序集合。
2. 数组的四个基本特点:
-- 其长度是确定的
-- 其元素必须是相同类型
-- 可以存储基本数据类型和引用数据类型
-- 数组变量属于引用类型
3. 一维数组的声明方式
-- type[] arr_name; (推荐使用这种方式)
-- type arr_name[]。
4. 数组的初始化:静态初始化、动态初始化和默认初始化。
5. 数组的长度:数组名.length,下标的合法区间[0,数组名.length-1]。
6. 数组拷贝:System类中的static void arraycopy(object src,int srcpos,object dest, int destpos,int length)方法。
7. 数组操作的常用类java.util.Arrays类
-- 打印数组:Arrays.toString(数组名);
-- 数组排序:Arrays.sort(数组名);
-- 二分查找:Arrays.binarySearch(数组名,查找的元素)。
8. 二维数组的声明
-- type[][]arr_name=new type[length][];
-- type arr_name[][]=new type[length][length]。
前面学习的8种基本数据类型并不是对象,为了实现基本数据类型和对象之间的相互转化,JDK为了每个基本数据类型都提供了相应的包装类。
Java是面向对象的语言,但并不是“纯面向对象”的,因为我们经常用到的基本数据类型就不是对象。但是我们在实际应用中经常需要将基本数据转化成对象,以便于操作。比如:将基本数据类型存储到Object[]数组或集合中的操作等等。
为了解决这个不足,Java在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。
包装类均位于java.lang包,八种包装类和基本数据类型的对应关系如表8-1所示:
在这八个类名中,除了Integer和Character类以外,其它六个类的类名和基本数据类型一致,只是类名的第一个字母大写而已。
在这八个类中,除了Character和Boolean以外,其他的都是“数字型”,“数字型”都是java.lang.Number的子类。Number类是抽象类,因此它的抽象方法,所有子类都需要提供实现。Number类提供了抽象方法:intValue()、longValue()、floatValue()、doubleValue(),意味着所有的“数字型”包装类都可以互相转型。
【示例】初识包装类
1 2 3 4 5 6 |
|
示例8-1的内存分析如图8-3所示:
对于包装类来说,这些类的用途主要包含两种:
1. 作为和基本数据类型对应的类型存在,方便涉及到对象的操作,如Object[]、集合等的操作。
2. 包含每种基本数据类型的相关属性如最大值、最小值等,以及相关的操作方法(这些操作方法的作用是在基本数据类型、包装类对象、字符串之间提供相互之间的转化!)。
【示例】包装类的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
执行结果如图所示:
自动装箱和拆箱就是将基本数据类型和包装类之间进行自动的互相转换。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所示。
【示例】自动装箱
1 2 3 |
|
【示例】自动拆箱
1 2 3 4 |
|
所以自动装箱与拆箱的功能是所谓的“编译器蜜糖(Compiler Sugar)”,虽然使用这个功能很方便,但在程序运行阶段您得了解Java的语义。例如示例8-5所示的程序是可以通过编译的:
【示例】包装类空指针异常问题
1 2 3 4 5 6 |
|
执行结果如图所示:
图示例 运行效果图
示例的运行结果之所以会出现空指针异常,是因为示例8-5中的代码相当于:
1 2 3 4 5 6 7 |
|
null表示i没有指向任何对象的实体,但作为对象名称是合法的(不管这个对象名称存是否指向了某个对象的实体)。由于实际上i并没有指向任何对象的实体,所以也就不可能操作intValue()方法,这样上面的写法在运行时就会出现NullPointerException错误。
【示例】自动装箱与拆箱
1 2 3 4 5 6 7 8 9 10 11 12 |
|
String类、StringBuilder类、StringBuffer类是三个与字符串相关的类。String类对象代表的是不可变字符序列,StringBuilder类、StringBuffer类代表可变字符序列。
String 类对象代表不可变的Unicode字符序列,因此我们可以将String对象称为“不可变对象”。 那什么叫做“不可变对象”呢?指的是对象内部的成员变量的值无法再改变。我们打开String类的源码,如图所示:
图 String类的部分源码
我们发现字符串内容全部存储到value[]数组中,而变量value是final类型的,也就是常量(即只能被赋值一次)。 这就是“不可变对象”的典型定义方式。
我们发现在前面学习String的某些方法,比如:substring()是对字符串的截取操作,但本质是读取原字符串内容生成了新的字符串。测试代码如下:
【示例】String测试代码
1 2 3 4 5 6 7 8 9 10 |
|
执行结果如图所示:
图 示例运行效果图
在遇到字符串常量之间的拼接时,编译器会做出优化,即在编译期间就会完成字符串的拼接。因此,在使用==进行String对象之间的比较时,我们需要特别注意,如示例所示。
【示例】字符串常量拼接时的优化
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
执行结果如图所示:
图 示例 运行效果图
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()。
StringBuffer和StringBuilder非常类似,均代表可变的字符序列。 这两个类都是抽象类AbstractStringBuilder的子类,方法几乎一模一样。我们打开AbstractStringBuilder的源码,如示例8-11所示:
【示例】AbstractStringBuilder 部分源码
1 2 3 4 5 6 7 |
|
显然,内部也是一个字符数组,但这个字符数组没有用final修饰,随时可以修改。因此,StringBuilder和StringBuffer称之为“可变字符序列”。那两者有什么区别呢?
1. StringBuffer JDK1.0版本提供的类,线程安全,做线程同步检查, 效率较低。
2. StringBuilder JDK1.5版本提供的类,线程不安全,不做线程同步检查,因此效率较高。 建议采用该类。
· 常用方法列表:
1. 重载的public StringBuilder append(…)方法
可以为该StringBuilder 对象添加字符序列,仍然返回自身对象。
2. 方法 public StringBuilder delete(int start,int end)
可以删除从start开始到end-1为止的一段字符序列,仍然返回自身对象。
3. 方法 public StringBuilder deleteCharAt(int index)
移除此序列指定位置上的 char,仍然返回自身对象。
4. 重载的public StringBuilder insert(…)方法
可以为该StringBuilder 对象在指定位置插入字符序列,仍然返回自身对象。
5. 方法 public StringBuilder reverse()
用于将字符序列逆序,仍然返回自身对象。
6. 方法 public String toString() 返回此序列中数据的字符串表示形式。
7. 和 String 类含义类似的方法:
1 2 3 4 5 6 |
|
【示例】StringBuffer/StringBuilder基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
执行结果如图所示:
· String使用的陷阱
String一经初始化后,就不会再改变其内容了。对String字符串的操作实际上是对其副本(原始拷贝)的操作,原来的字符串一点都没有改变。比如:
String s ="a"; 创建了一个字符串
s = s+"b"; 实际上原来的"a"字符串对象已经丢弃了,现在又产生了另一个字符串s+"b"(也就是"ab")。 如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的时间和空间性能,甚至会造成服务器的崩溃。
相反,StringBuilder和StringBuffer类是对原字符串本身操作的,可以对字符串进行修改而不产生副本拷贝或者产生少量的副本。因此可以在循环中使用。
【示例】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 |
|
执行结果如图所示:
图 示例运行效果图
要点:
1. String:不可变字符序列。
2. StringBuffer:可变字符序列,并且线程安全,但是效率低。
3. StringBuilder:可变字符序列,线程不安全,但是效率高(一般用它)。
“时间如流水,一去不复返”,时间是一个一维的东东。所以,我们需要一把刻度尺来表达和度量时间。在计算机世界,我们把1970 年 1 月 1 日 00:00:00定为基准时间,每个度量单位是毫秒(1秒的千分之一),如图8-13所示。
图 计算机的时间概念
我们用long类型的变量来表示时间,从基准时间往前几亿年,往后几亿年都能表示。如果想获得现在时刻的“时刻数值”,可以使用:
1 |
|
这个“时刻数值”是所有时间类的核心值,年月日都是根据这个“数值”计算出来的。我们工作学习涉及的时间相关类有如下这些:
在标准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)。
【示例】Date类的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
执行结果如图所示:
图示例 运行效果图
查看API文档大家可以看到其实Date类中的很多方法都已经过时了。JDK1.1之前的Date包含了:日期操作、字符串转化成时间对象等操作。JDK1.1之后,日期操作一般使用Calendar类,而字符串的转化使用DateFormat类。
·DateFormat类的作用
把时间对象转化成指定格式的字符串。反之,把指定格式的字符串转化成时间对象。
DateFormat是一个抽象类,一般使用它的的子类SimpleDateFormat类来实现。
【示例 】DateFormat类和SimpleDateFormat类的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
执行结果如图所示:
图 示例 运行效果图
代码中的格式化字符的具体含义见表:
时间格式字符也可以为我们提供其他的便利。比如:获得当前时间是今年的第几天。代码如下:
【示例 】时间格式字符的使用
1 2 3 4 5 6 7 8 9 |
|
执行结果如图 所示:
Calendar 类是一个抽象类,为我们提供了关于日期计算的相关功能,比如:年、月、日、时、分、秒的展示和计算。
GregorianCalendar 是 Calendar 的一个具体子类,提供了世界上大多数国家/地区使用的标准日历系统。
菜鸟雷区
注意月份的表示,一月是0,二月是1,以此类推,12月是11。 因为大多数人习惯于使用单词而不是使用数字来表示月份,这样程序也许更易读,父类Calendar使用常量来表示月份:JANUARY、FEBRUARY等等。
【示例 】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 |
|
执行结果如图所示:
图 示例 运行效果图
编写程序,利用GregorianCalendar类,打印当前月份的日历,今天的日期是 2017-05-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 |
|
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) 角度->弧度
【示例 】Math类的常用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
执行结果如图所示:
图 示例运行效果图
Math类中虽然为我们提供了产生随机数的方法Math.random(),但是通常我们需要的随机数范围并不是[0, 1)之间的double类型的数据,这就需要对其进行一些复杂的运算。如果使用Math.random()计算过于复杂的话,我们可以使用例外一种方式得到随机数,即Random类,这个类是专门用来生成随机数的,并且Math.random()底层调用的就是Random的nextDouble()方法。
【示例】Random类的常用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
执行结果如图所示:
图 示例运行效果图
注意
Random类位于java.util包下。
File类用来代表文件和目录。
java.io.File类:代表文件和目录。 在开发中,读取文件、生成文件、删除文件、修改文件的属性时经常会用到本类。
File类的常见构造方法:public File(String pathname)
以pathname为路径创建File对象,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储,如示例 所示。
【示例8-21】文件的创建
1 2 3 4 5 6 7 8 9 10 |
|
在eclipse项目开发中,user.dir就是本项目的目录。因此,执行完毕后,在本项目和D盘下都生成了新的文件(如果是eclipse下,一定按F5刷新目录结构才能看到新文件)。如图 所示。
图 本项目目录中新增文件效果
通过File对象可以访问文件的属性:
表 File类访问属性的方法列表
【示例 】测试File类访问属性的基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
执行结果如图 所示:
图 示例 运行效果图
通过File对象创建空文件或目录(在该对象所指的文件或目录不存在的情况下)
表 File类创建文件或目录的方法列表
【示例 】使用mkdir创建目录
1 2 3 4 5 6 7 8 9 10 11 |
|
执行结果如图 所示:
图 示例 运行效果图
【示例 】使用mkdirs创建目录
1 2 3 4 5 6 7 8 9 10 11 |
|
执行结果如图 所示:
图 示例 运行效果图
【示例 】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 |
|
第一次执行结果如图 所示:
图 示例 第一次运行效果图
第二次执行结果如图 所示:
本节结合前面给大家讲的递归算法,展示目录结构。大家可以先建立一个目录,下面增加几个子文件夹或者文件,用于测试。
【示例 】使用递归算法,以树状结构展示目录树
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 |
|
执行结果如图 所示:
JDK1.5引入了枚举类型。枚举类型的定义包括枚举声明和枚举体。格式如下:
1 2 3 |
|
枚举体就是放置一些常量。我们可以写出我们的第一个枚举类型,如示例 所示:
【示例 】创建枚举类型
1 2 3 |
|
所有的枚举类型隐性地继承自 java.lang.Enum。枚举实质上还是类!而每个被枚举的成员实质就是一个枚举类型的实例,他们默认都是public static final修饰的。可以直接通过枚举类型名使用它们。
老鸟建议
1. 当你需要定义一组常量时,可以使用枚举类型。
2. 尽量不要使用枚举的高级特性,事实上高级特性都可以使用普通类来实现,没有必要引入枚举,增加程序的复杂性!
【示例 】枚举的使用
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 |
|
1. 每一个基本数据类型对应一个包装类。
2. 包装类的用途:
作为和基本数据类型对应的引用类型存在,方便涉及到对象的操作。
包含每种基本数据类型的相关属性如最大值、最小值以及相关的操作方法。
3. JDK1.5后在Java中引入自动装箱和拆箱。
4. 字符串相关类String、StringBuffer与StringBuilder
String:不可变字符序列。
StringBuffer:可变字符序列,并且线程安全,但是效率低。
StringBuilder:可变字符序列,线程不安全,但是效率高(一般用它)。
日期与时间类Date、DateFormat、SimpleDateFormat、Calendar、GregorianCalendar。
5. Math类的常用方法
pow(double a,double b)
max(double a,double b)
min(double a,double b)
random()
long round(double a)
6. 与操作文件相关的File类。
7. 当需要定义一组常量时,使用枚举类型。
开发和学习中需要时刻和数据打交道,如何组织这些数据是我们编程中重要的内容。 我们一般通过“容器”来容纳和管理数据。那什么是“容器”呢?生活中的容器不难理解,是用来容纳物体的,如锅碗瓢盆、箱子和包等。程序中的“容器”也有类似的功能,就是用来容纳和管理数据。
事实上,我们第七章所学的数组就是一种容器,可以在其中放置对象或基本类型数据。
数组的优势:是一种简单的线性序列,可以快速地访问数组元素,效率高。如果从效率和类型检查的角度讲,数组是最好的。
数组的劣势:不灵活。容量需要事先定义好,不能随着需求的变化而扩容。比如:我们在一个用户管理系统中,要把今天注册的所有用户取出来,那么这样的用户有多少个?我们在写程序时是无法确定的。因此,在这里就不能使用数组。
基于数组并不能满足我们对于“管理和组织数据的需求”,所以我们需要一种更强大、更灵活、容量随时可扩的容器来装载我们的对象。 这就是我们今天要学习的容器,也叫集合(Collection)。以下是容器的接口层次结构图:
图 容器的接口层次结构图
为了能够更好的学习容器,我们首先要先来学习一个概念:泛型。
泛型是JDK1.5以后增加的,它可以帮助我们建立类型安全的集合。在使用了泛型的集合中,遍历时不必进行强制类型转换。JDK提供了支持泛型的编译器,将运行时的类型检查提前到了编译时执行,提高了代码可读性和安全性。
泛型的本质就是“数据类型的参数化”。 我们可以把“泛型”理解为数据类型的一个占位符(形式参数),即告诉编译器,在调用泛型时必须传入实际类型。
我们可以在类的声明处增加泛型列表,如:
【示例 】泛型类的声明
1 2 3 4 5 6 7 8 9 10 |
|
泛型E像一个占位符一样表示“未知的某个数据类型”,我们在真正调用的时候传入这个“数据类型”。
【示例 】泛型类的应用
1 2 3 4 5 6 7 8 9 10 |
|
容器相关类都定义了泛型,我们在开发和工作中,在使用容器类时都要使用泛型。这样,在容器的存储数据、读取数据时都避免了大量的类型判断,非常便捷。
【示例 】泛型类的在集合中的使用
1 2 3 4 5 6 7 8 9 |
|
通过阅读源码,我们发现Collection、List、Set、Map、Iterator接口都定义了泛型,如下图所示:
图 容器的泛型定义
因此,我们在使用这些接口及其实现类时,都要使用泛型。
菜鸟雷区
我们只是强烈建议使用泛型。事实上,不使用编译器也不会报错!
Collection 表示一组对象,它是集中、收集的意思。Collection接口的两个子接口是List、Set接口。
表 Collection接口中定义的方法
由于List、Set是Collection的子接口,意味着所有List、Set的实现类都有上面的方法。我们下一节中,通过ArrayList实现类来测试上面的方法。
List是指有顺序、可重复的容器。List接口是Collection接口的子接口,因此Cooleciton中的方法List也都会有;同时,List接口增加了和顺序相关的方法。
List是有序、可重复的容器。
有序:List中每个元素都有索引标记。可以根据元素的索引标记(在List中的位置)访问元素,从而精确控制这些元素。
可重复:List允许加入重复的元素。更确切地讲,List通常允许满足 e1.equals(e2) 的元素重复加入容器。
除了Collection接口中的方法,List多了一些跟顺序(索引)有关的方法,参见下表:
表 List接口中定义的方法
List接口常用的实现类有3个:ArrayList、LinkedList和Vector。
【示例 】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 |
|
执行结果如图 所示:
图 示例 运行效果图
【示例 】两个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 |
|
执行结果如图 所示:
图 示例 运行效果图
【示例 】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 |
|
执行结果如图 所示:
ArrayList底层是用数组实现的存储。 特点:查询效率高,增删效率低,线程不安全。我们一般使用它。查看源码:
图 ArrayList底层源码(1)
我们可以看出ArrayList底层使用Object数组来存储元素数据。所有的方法,都围绕这个核心的Object数组来开展。
我们知道,数组长度是有限的,而ArrayList是可以存放任意数量的对象,长度不受限制,那么它是怎么实现的呢?本质上就是通过定义新的更大的数组,将旧数组中的内容拷贝到新数组,来实现扩容。 ArrayList的Object数组初始化长度为10,如果我们存储满了这个数组,需要存储第11个对象,就会定义新的长度更大的数组,并将原数组内容和新的元素一起加入到新数组中,源码如下:
LinkedList底层用双向链表实现的存储。特点:查询效率低,增删效率高,线程不安全。
双向链表也叫双链表,是链表的一种,它的每个数据节点中都有两个指针,分别指向前一个节点和后一个节点。 所以,从双向链表中的任意一个节点开始,都可以很方便地找到所有节点。
图 LinkedList的存储结构图
每个节点都应该有3部分内容:
1 2 3 4 5 |
|
我们查看LinkedList的源码,可以看到里面包含了双向链表的相关代码:
图 LinkedList的底层源码
注意事项
entry在英文中表示“进入、词条、条目”的意思。在计算机英语中一般表示“项、条目”的含义。
Vector底层是用数组实现的List,相关的方法都加了同步检查,因此“线程安全,效率低”。 比如,indexOf方法就增加了synchronized同步标记。
图 Vector的底层源码
老鸟建议
如何选用ArrayList、LinkedList、Vector?
1. 需要线程安全时,用Vector。
2. 不存在线程安全问题时,并且查找较多用ArrayList(一般使用它)。
3. 不存在线程安全问题时,增加或删除元素较多用LinkedList。
现实生活中,我们经常需要成对存储某些信息。比如,我们使用的微信,一个手机号只能对应一个微信账户。这就是一种成对存储的关系。
Map就是用来存储“键(key)-值(value) 对”的。 Map类中存储的“键值对”通过键来标识,所以“键对象”不能重复。
Map 接口的实现类有HashMap、TreeMap、HashTable、Properties等。
表 Map接口中常用的方法
HashMap采用哈希算法实现,是Map接口最常用的实现类。 由于底层采用了哈希表存储数据,我们要求键不能重复,如果发生重复,新的键值对会替换旧的键值对。 HashMap在查找、删除、修改方面都有非常高的效率。
【示例 】Map接口中的常用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
执行结果如图 所示:
图 示例 运行效果图
HashTable类和HashMap用法几乎一样,底层实现几乎一样,只不过HashTable的方法添加了synchronized关键字确保线程同步检查,效率较低。
HashMap与HashTable的区别
1. HashMap: 线程不安全,效率高。允许key或value为null。
2. HashTable: 线程安全,效率低。不允许key或value为null。
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 |
|
运行如上程序,我们就能发现直接取余(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时,链表就转换为红黑树,这样又大大提高了查找的效率。
下一节,我们简单介绍一个二叉树。同时,也便于大家理解TreeMap的底层结构。
二叉树的定义
二叉树是树形结构的一个重要类型。 许多实际问题抽象出来的数据结构往往是二叉树的形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。
二叉树(BinaryTree)由一个节点及两棵互不相交的、分别称作这个根的左子树和右子树的二叉树组成。下图中展现了五种不同基本形态的二叉树。
图 二叉树五种基本形态示意图
(a) 为空树。
(b) 为仅有一个结点的二叉树。
(c) 是仅有左子树而右子树为空的二叉树。
(d) 是仅有右子树而左子树为空的二叉树。
(e) 是左、右子树均非空的二叉树。
注意事项
二叉树的左子树和右子树是严格区分并且不能随意颠倒的,图 (c) 与图 (d) 就是两棵不同的二叉树。
排序二叉树特性如下:
(1) 左子树上所有节点的值均小于它的根节点的值。
(2) 右子树上所有节点的值均大于它的根节点的值。
比如:我们要将数据【14,12,23,4,16,13, 8,,3】存储到排序二叉树中,如下图所示:
图 排序二叉树示意图(1)
排序二叉树本身实现了排序功能,可以快速检索。但如果插入的节点集本身就是有序的,要么是由小到大排列,要么是由大到小排列,那么最后得到的排序二叉树将变成普通的链表,其检索效率就会很差。 比如上面的数据【14,12,23,4,16,13, 8,,3】,我们先进行排序变成:【3,4,8,12,13,14,16,23】,然后存储到排序二叉树中,显然就变成了链表,如下图所示:
图 排序二叉树示意图(2)
▪ 平衡二叉树(AVL)
为了避免出现上述一边倒的存储,科学家提出了“平衡二叉树”。
在平衡二叉树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。 增加和删除节点可能需要通过一次或多次树旋转来重新平衡这个树。
节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子 -2或2的节点被认为是不平衡的,并需要重新平衡这个树。
比如,我们存储排好序的数据【3,4,8,12,13,14,16,23】,增加节点如果出现不平衡,则通过节点的左旋或右旋,重新平衡树结构,最终平衡二叉树如下图所示:
图 平衡二叉树示意图
平衡二叉树追求绝对平衡,实现起来比较麻烦,每次插入新节点需要做的旋转操作次数不能预知。
▪ 红黑二叉树
红黑二叉树(简称:红黑树),它首先是一棵二叉树,同时也是一棵自平衡的排序二叉树。
红黑树在原有的排序二叉树增加了如下几个要求:
1. 每个节点要么是红色,要么是黑色。
2. 根节点永远是黑色的。
3. 所有的叶节点都是空节点(即 null),并且是黑色的。
4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)
5. 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。
这些约束强化了红黑树的关键性质:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。这样就让树大致上是平衡的。
红黑树是一个更高效的检索二叉树,JDK 提供的集合类 TreeMap、TreeSet 本身就是一个红黑树的实现。
图9-22一个典型的红黑树(考虑书本印刷问题,浅色表示红色,深色表示黑色)
红黑树的基本操作:插入、删除、左旋、右旋、着色。 每插入或者删除一个节点,可能会导致树不在符合红黑树的特征,需要进行修复,进行 “左旋、右旋、着色”操作,使树继续保持红黑树的特性。
9.4.4 TreeMap的使用和底层实现
TreeMap是红黑二叉树的典型实现。我们打开TreeMap的源码,发现里面有一行核心代码:
1 |
|
root用来存储整个树的根节点。我们继续跟踪Entry(是TreeMap的内部类)的代码:
图 Entry底层源码
可以看到里面存储了本身数据、左节点、右节点、父节点、以及节点颜色。 TreeMap的put()/remove()方法大量使用了红黑树的理论。本书限于篇幅,不再展开。需要了解更深入的,可以参考专门的数据结构书籍。
TreeMap和HashMap实现了同样的接口Map,因此,用法对于调用者来说没有区别。HashMap效率高于TreeMap;在需要排序的Map时才选用TreeMap。
Set接口继承自Collection,Set接口中没有新增方法,方法和Collection保持完全一致。我们在前面通过List学习的方法,在Set中仍然适用。因此,学习Set的使用将没有任何难度。
Set容器特点:无序、不可重复。无序指Set中的元素没有索引,我们只能遍历查找;不可重复指不允许加入重复的元素。更确切地讲,新元素如果和Set中某个元素通过equals()方法对比为true,则不能加入;甚至,Set中也只能放入一个null元素,不能多个。
Set常用的实现类有:HashSet、TreeSet等,我们一般使用HashSet。
大家在做下面练习时,重点体会“Set是无序、不可重复”的核心要点。
【示例 】HashSet的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
执行结果如图 所示:
HashSet是采用哈希算法实现,底层实际是用HashMap实现的(HashSet本质就是一个简化版的HashMap),因此,查询效率和增删效率都比较高。我们来看一下HashSet的源码:
图 HashSet底层源码
我们发现里面有个map属性,这就是HashSet的核心秘密。我们再看add()方法,发现增加一个元素说白了就是在map中增加一个键值对,键对象就是这个元素,值对象是名为PRESENT的Object对象。说白了,就是“往set中加入元素,本质就是把这个元素作为key加入到了内部的map中”。
由于map中key都是不可重复的,因此,Set天然具有“不可重复”的特性。
TreeSet底层实际是用TreeMap实现的,内部维持了一个简化版的TreeMap,通过key来存储Set的元素。 TreeSet内部需要对存储的元素进行排序,因此,我们对应的类需要实现Comparable接口。这样,才能根据compareTo()方法比较对象之间的大小,才能进行内部排序。
【示例 】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 |
|
使用TreeSet要点:
(1) 由于是二叉树,需要对元素做内部排序。 如果要放入TreeSet中的类没有实现Comparable接口,则会抛出异常:java.lang.ClassCastException。
(2) TreeSet中不能放入null元素。
Iterator接口可以让开发者实现对容器中对象的遍历。
所有实现了Collection接口的容器类都有一个Iterator方法用以返回一个实现了Iterator接口的对象。
Iterarot对象被称为迭代器,可用于方便的实现对容器内元素的遍历,Iterator接口定义了如下方法:
迭代器为我们提供了统一的遍历容器的方式,参见以下示例代码:
【示例 】迭代器遍历List
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
执行结果如图 所示:
图 示例 运行效果图
老鸟建议
如果遇到遍历容器时,判断删除元素的情况,使用迭代器遍历!
【示例 】迭代器遍历Set
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
执行结果如图 所示:
图 示例 运行效果图
【示例 】迭代器遍历Map一
1 2 3 4 5 6 7 8 9 10 11 12 |
|
执行结果如图 所示:
图 示例 运行效果图
我们也可以通过map的keySet()、valueSet()获得key和value的集合,从而遍历它们。
【示例 】迭代器遍历Map二
1 2 3 4 5 6 7 8 9 10 11 12 |
|
执行结果如图 所示:
【示例 】遍历List方法一:普通for循环
1 2 3 4 |
|
【示例 】遍历List方法二:增强for循环(使用泛型!)
1 2 3 |
|
【示例 】遍历List方法三:使用Iterator迭代器(1)
1 2 3 4 |
|
【示例 】遍历List方法四:使用Iterator迭代器(2)
1 2 3 4 5 6 |
|
【示例 】遍历Set方法一:增强for循环
1 2 3 |
|
【示例 】遍历Set方法二:使用Iterator迭代器
1 2 3 4 |
|
【示例 】遍历Map方法一:根据key获取value
1 2 3 4 5 |
|
【示例 】遍历Map方法二:使用entrySet
1 2 3 4 |
|
类 java.util.Collections 提供了对Set、List、Map进行排序、填充、查找元素的辅助方法。
1. void sort(List) //对List容器内的元素排序,排序的规则是按照升序进行排序。
2. void shuffle(List) //对List容器内的元素进行随机排列。
3. void reverse(List) //对List容器内的元素进行逆续排列 。
4. void fill(List, Object) //用一个特定的对象重写整个List容器。
5. int binarySearch(List, Object)//对于顺序的List容器,采用折半查找的方法查找特定对象。
【示例 】Collections工具类的常用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
执行结果如图 所示:
1. Collection 表示一组对象,它是集中、收集的意思,就是把一些数据收集起来。
2. Collection接口的两个子接口:
1) List中的元素有顺序,可重复。常用的实现类有ArrayList、LinkedList和 vector。
Ø ArrayList特点:查询效率高,增删效率低,线程不安全。
Ø LinkedList特点:查询效率低,增删效率高,线程不安全。
Ø vector特点:线程安全,效率低,其它特征类似于ArrayList。
2) Set中的元素没有顺序,不可重复。常用的实现类有HashSet和TreeSet。
Ø HashSet特点:采用哈希算法实现,查询效率和增删效率都比较高。
Ø TreeSet特点:内部需要对存储的元素进行排序。因此,我们对应的类需要实现Comparable接口。这样,才能根据compareTo()方法比较对象之间的大小,才能进行内部排序。
3. 实现Map接口的类用来存储键(key)-值(value) 对。Map 接口的实现类有HashMap和TreeMap等。Map类中存储的键-值对通过键来标识,所以键值不能重复。
4. Iterator对象称作迭代器,用以方便的实现对容器内元素的遍历操作。
5. 类 java.util.Collections 提供了对Set、List、Map操作的工具方法。
6. 如下情况,可能需要我们重写equals/hashCode方法:
1) 要将我们自定义的对象放入HashSet中处理。
2) 要将我们自定义的对象作为HashMap的key处理。
3) 放入Collection容器中的自定义对象后,可能会调用remove、contains等方法时。
7. JDK1.5以后增加了泛型。泛型的好处:
1) 向集合添加数据时保证数据安全。
2) 遍历集合元素时不需要强制转换。