Java核心技术 卷1
Java程序概述
关键术语
- 简单性
- 面向对象:将重点放在数据(即对象)和对象的接口上
- 网络技能
- 健壮性:能检测许多在其他语言仅在运行时刻才能够检测出来的问题
- 安全性:能防范运行时堆栈溢出,在自己的处理空间之外破坏内存,未经授权读写文件
- 体系结构中立
- 可移植性
- 解释型:java解释器可以在任何移植了解释器的机器上运行java字节码
- 高性能
- 多线程:只要操作系统支持,Java中的线程就可以利用多个处理器
- 动态性
Java applet 与 Internet
在网页上运行Java程序称为applet
为了使用applet,需要启动java的web浏览器执行字节码。由于Sun公司负责发放Java源代码的许可证,并坚持不允许对语言和基本类库的结构做出任何修改。因此,Java的applet应该可以运行在任何启动Java浏览器上,并且无论何时访问包含applet的网页,都会得到程序的最终版本。
配置环境
- JDK安装
- 将jdk/bin目录添加到执行路径中,所谓执行路径是操作系统搜索本地可执行文件的目录列表
- 安装库源文件和文档
导航Java目录
bin 编译器和工具
docs HTML格式的类库文件
include 用于编译本地方法的文件
jre java运行环境文件
lib 类库文件
src 类库源文件
Java的基本程序设计结构
public class FirstSample{
public static void main(String[] args){
System.out.println("We will not use ‘Hello,World!’");
}
}
关键字public
称为访问修饰符,它用于控制程序的其他部分对这段代码的访问规则。
关键字class
表明Java程序中的全部内容都包含在类中,这里只需要将类作为一个加载程序逻辑的容器。
数据类型
在Java中,一共有8种基本类型,其中有4种整型、2种浮点类型、1种用于表示Unicode编码的字符单元的字符类型char和1种用于表示真值的boolean类型
整型
类型 | 存储需求 |
---|---|
int | 4字节 |
short | 2字节 |
long | 8字节 |
byte | 1字节 |
长整型数值有一个后缀L,十六进制有一个前缀0x,从Java7开始,加上前缀0b就可以写二进制数。同样是从Java7开始,还可以为数字字面量加下划线,这些下划线只是为了让人更易读,Java编译器会去除这些下划线
浮点类型
浮点类型用于表示有小数部分的数值。在Java中有两种浮点类型
类型 | 存储需求 |
---|---|
float | 4字节 |
double | 8字节 |
double表示这种类型的数值精度是float类型的两倍。绝大部分应用程序都采用double类型。在很多情况下,float类型的精度很难满足需求。只有很少的情况适合使用float类型,float类型的数值有一个后缀F,没有后缀F的默认为double类型,也可以在浮点数值后面加后缀D。
char类型
char类型用于表示单个字符,通常用来表示字符常量。
转义序列 | 名称 | Unicode值 |
---|---|---|
\b | 退格 | \u0008 |
\f | 制表 | \u0009 |
\n | 换行 | \u000a |
\f | 回车 | \u000d |
\ " | 双引号 | \n0022 |
\ ’ | 单引号 | \u0027 |
\ \ | 反斜杠 | \u005c |
Unicode打破了传统字符编码方法的限制。
Unicode-16采用不同长度的编码表示所有Unicode代码点(是指与一个编码表中的某个字符对应的代码值)。在Java中,char类型用UTF-16编码描述一个代码单元
boolean类型
整型值和布尔值之间不能相互转换
变量
在Java中,每一个变量属于一种类型。在声明变量时,变量所属的类位于变量名之前。
在Java中,利用关键字final指示常量、关键字final表示这个变量只能被赋值一次,一旦被赋值之后,就不能再更改了。习惯上,变量名使用全大写。
在Java中,经常希望某个常量可以在一个类中的多个方法中使用,通常将这些变量称为类常量。可以使用关键字static final
设置一个类常量。
运算符
当参与除运算的两个操作数都是整数时,表示整数除法,否则表示浮点除法。
位运算符
在处理整型数值时,可以直接对组成整型数值的各个位进行操作。这意味着可以使用屏蔽技术获得整数中的各个位。
>> 和 << 运算符将二进制进行右移和左移操作,当需要建立位模式屏蔽某些位时,使用这两个运算符十分方便。
>>>运算符将用0填充高位,>>运算符用符号位填充高位,没有<<<运算符
数值类型之间的转换
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OZr3nm1Y-1619158617756)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210405155256593.png)]
图中有6个实心箭头,表示无消息丢失的转换。有3个虚箭头,表示可能有精度损失的转换。
枚举类型
有时候,变量的取值只在一个有限的集合内。针对这种情况,可以自定义枚举类型。枚举类型包括有限个命名的值。
字符串
当将一个字符串与一个非字符串的值进行拼接时,后者被转换成字符串。
各种字符串放在公共的存储池中,字符串变量指向存储池中的相应为止,如果复制一个字符串变量,原始字符串与复制的字符串共享相同的字符‘、
//构建字符串
StringBuilder builder = new StringBuilder();//构造一个空字符串
//每次需要添加一部分的内容,就调用append方法
builder.append(ch);//appends a single character
bulider.append(str);//append a string
输入输出
//构造一个Scanner对象,并于标准输入流 System.in 关联
Scanner in = new Scanner(System.in);
String name = in.nextLine();//使用nextLine()方法是因为在输入行中可能包含空格,如果读取单词就用next()方法,读取整数用nextInt()方法
用于printf的转换符
转换符 | 类型 |
---|---|
d | 十进制整数 |
x | 十六进制整数 |
o | 八进制整数 |
f | 定点浮点数 |
e | 指数浮点数 |
g | 通用浮点数 |
a | 十六进制浮点数 |
s | 字符串 |
c | 字符 |
b | 布尔 |
h | 散列码 |
tx | 日期时间 |
% | 百分号 |
n | 与平台有关的行分割符 |
用于printf的标志
标志 | 目的 |
---|---|
+ | 打印正数和负数的符号 |
空格 | 在正数之前添加空格 |
0 | 数字前面补0 |
- | 左对齐 |
( | 将负数括在括号里 |
. | 添加分组分隔符 |
#(对于f格式) | 包含小数点 |
#(对于x或0格式) | 添加前缀0x或0 |
$ | 给定被格式化的参数索引 |
< | 格式化前面说明的数值 |
文件输入与输出
//需要用File对象构造一个Scanner对象
Scanner in = new Scanner.get(Paths.get("myfile.txt"));
//写入文件,如果文件不存在,则创建该文件
PrintWrite out = new PrintWriter("myfile.txt")
带标签的break语句
标签必须放在希望跳出的最外层循环之前,并且必须紧跟一个冒号
read_data();
{
{{
break read_data;
}
}}
控制流程
块作用域:块是指一对花括号括起来的若干条简单的Java语句。块确定了变量的作用域。一个块可以嵌套在另一个块中
大数值
如果基本的整数和浮点数精度不能够满足需求,那么可以使用java.math包中的两个很有用的类:BigInteger
和BigDecimal
这两个类可以处理包含任意长度数字序列的数值。BigInteger类实现了任意精度的整数运算,BigDecimal实现了任意精度的浮点数运算。
数组
for each循环:Java有一种功能很强的循环结构,可以用来依次处理数组中的每个元素(其他类型的元素集合亦可)而不必为指定下标值而分心。
语句格式为:
for (variable : collection) statement
打印数组中所有值可以利用Arrays类的toString方法,返回一个包含数组元素的字符串,这些元素被放置在括号内,并用逗号分隔。
快速打印二维数组的数据元素列表可以调用:
System.out.println(Arrays.deepToString(a));
对象与类
面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。
类
类是构造对象的模板或蓝图。由类构造对象的过程称为创建类的实例
封装从形式上看,封装不过是将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方法。对象中的数据称为实例域,操纵数据的过程称为方法。对于每个特定的类实例都有一组特定的实例阈值。这些值的集合就是这个对象的当前状态。
实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。
对象
对象的三个特性:
- 对象的行为——可以对对象施加哪些操作,或可以对对象施加哪些方法
- 对象的状态——当施加那些方法时,对象如何响应
- 对象标识——如何辨别具有相同行为与状态的不同对象
类之间的关系:
- 依赖:一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类
- 聚合:类A的对象包含类B的对象
- 继承
javac EmployeeTest.java
当Java编译器发现EmplyeeTest.java使用了Employee类时会查找名为Employee.class的文件。如果没有找到这个文件,就会自动地搜索Employee.java,然后对它进行编译。更重要的是:如果Employee.java版本较已有的Employee.class文件版本新,Java编译器就会自动地重新编译这个文件。
构造器
构造器与类同名,在构造类的对象时,构造器会运行,以便将实例域初始化为所希望的状态。
构造器与其他的方法有一个重要的不同,构造器总是伴随着new操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。
隐式参数与显式参数
方法用于操作对象以及存取它们的实例域
public void raiseSalary(double byPercent){
double raise = salary * byPercent / 100;
salary += raise;
}
将调用这个方法的对象的salary实例域设置为新值。看看下面这个调用:
number007.raiseSalary(5);
它的结果将number007.salary域的值增加5%,具体地说,这个调用将执行下列指令:
double raise = number007.salary * 5 / 100;
number007.salary += raise;
raiseSalary方法有两个参数。第一个参数称为隐式参数,是出现在方法名前的Employee类对象。第二个参数位于方法名后面括号中的数值,这是一个显式参数。
在每一个方法中,关键字this
表示隐式参数,采用下面的方式可以将实例域与局部变量明显地区分开来。
public void raiseSalary(double byPercent){
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
在Java程序设计语言中,所有的方法都必须在类的内部定义,但并不表示它们是内联方法。是否将某个方法设置为内联方法是Java虚拟机的任务。即时编译器会监视调用那些简洁、经常被调用、没有被重载以及可优化的方法。
封装的优点:
getxxx方法是典型的访问器方法,由于它们只返回实例域值,因此又称为域访问器。一旦在构造器中设置完成,就没有任何一个方法可以对它进行修改,这样确保域不会受到外界的破坏。
在有时候需要获得或设置实例域的值,因此,应该提供下面三项内容:
- 一个私有的数据域
- 一个共有的域访问器方法
- 一个公用的域更改器方法
基于类的访问权限
一个方法可以访问所属类的所有对象的私有数据
私有方法
只要方法是私有的,类的设计者就可以确信,它不会被外部的其他类操作调用,可以将其删去。如果方法是公有的,就不能将其删去,因为其他的代码很可能依赖它。
final实例域
可以将实例域定位为final。构建对象时必须初始化这样的域。final修饰符大都应用于基本类型域或不可变域
静态域与静态方法
静态域
如果将域定义为static,每个类中只有一个这样的域,而每一个对象对所有的实例域却都有自己的一份拷贝。
静态常量
静态变量使用得比较少,但静态常量却使用得比较多。如果关键字static被省略,PI就变成了Math类的一个实例域,需要通过Math类的对象访问PI,并且每一个Math对象都有它自己的一份PI拷贝。
静态方法
静态方法是一种不能向对象实施操作的方法。可以认为静态方法是没有this参数的方法
因为静态方法不能操作对象,所以不能在静态方法中访问实例域,但是,静态方法可以访问自身类中的静态域。
工厂方法
静态方法还有一种常见的用途。NumberFormat类使用工程方法产生不同风格的格式对象。
方法参数
按值调用表示方法接收的是调用者提供的值。
而按引用调用表示方法接收的是调用者提供的变量地址。
方法参数共有两种类型:基本数据类型、对象引用
对象构造
重载
如果多个方法有相同的名字,不同的参数,便产生了重载。编译器必须挑选除具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所实验的值类型进行匹配来挑选出相应的方法。如果编译器找不到匹配的参数,或者找出多个可能的匹配,就会产生编译时错误。(这个过程被称为重载解析)
如果在构造器没有显式地给域赋予初值,那么就会被自动地赋为默认值:数值为0、布尔值为false、对象引用为null
如果在编写一个类的时候没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。
显式域初始化
由于类的构造器方法可以重载,所有可以采用多种形式设置类的实例域的初始状态。确保不管怎么样调用构造器,每个实例域都可以被设置为一个有意义的初值。
在执行构造器之前,先执行赋值操作。当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,这种方法特别有用。
参数名
还有一种常用的技巧,它基于这样的事实:参数变量用同样的名字将实例域屏蔽起来。
public Employee(String name, double salary){
this.name = name;
this.salary = salary;
}
如果将参数命名为salary,salary将引用这个参数,而不是实例域。但是,可以采用this.salary的形式访问实例域。
调用另一个构造器
public Employee(double s){
this("Employee #" + nextId, s);
nextId++;
}
如果构造器的第一个语句为this(…),这个构造器将调用同一个类的另一个构造器。
初始化块
调用构造器的具体处理步骤:
- 所有数据域被初始化为默认值
- 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块
- 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体
- 执行这个构造器的主体
可以通过一个初始化值,或者使用一个静态的初始化块对静态域进行初始化。
对象析构与finalize方法
可以为任何一个方法添加finalize方法。finalize方法将在垃圾回收器清楚对象之前调用,在实际应用中,不要依赖于使用finalize方法回收任何短缺的资源,这是因为很难知道这个方法什么时候调用。
如果某个资源需要在使用完毕后立即被关闭,那么就需要人工来管理。对象用完时,可以应用一个close方法来完成相应的清理操作。
包
Java允许使用包(package)将类组织起来。使用包的主要原因是确保类名的唯一性。
一个类可以使用所属的包中的所有类,以及其他包中的公有类。
静态导入
import可以导入静态方法和静态域的功能
包作用域
如果没有指定Public和private,这个部分可以被同一个包的所有方法访问。
类设计技巧
- 一定要保证数据私有
- 一定要对数据初始化
- 不要在类中使用过多的基本类型
- 不是所有的域都需要独立的域访问器和域更改器
- 将职责过多的类进行分解
- 类名和方法名要能够体现它们的职责
继承
类、超类和子类
关键字extends
表明正在构造的新类派生于一个已存在的类。已存在的类称为超类、基类或父类;新类称为子类、派生类或孩子类。
在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。因此在设计类的时候,应该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中。
继承层次
由一个公共超类派生出来的所有类的集合被称为继承层次。在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链。
多态
有一个用来判断是否应该设计为继承关系的简单规则,这就是"is-a"规则,它表明子类的每个对象也是超类的对象。另一种表述法是置换规则,它表明程序中出现超类对象的任何地方都可以用子类对象置换。
例如:可以将一个子类的对象赋给超类变量
Employee e;
e = new Employee(...)//Employee object expected
e = new Manager(...)//OK, Manager can be used as well
在JAVA中,对象变量是多态的。一个Employee变量既可以引用以恶搞Employee类对象,也可以引入一个Employee类的任何一个子类的对象
动态绑定
- 编译器查看对象的声明类型和方法名。假设调用x.f(param),且隐式参数x声明为C类的对象。需要注意的是可能存在多个名字为f,但参数类型不一样的方法,编译器将会一一列举所有C类中名为f的方法和其超类中访问属性是public且名为f的方法(超类的私有方法不可调用)
- 接下来,编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为"参数解析"。由于允许类型转换,所以这个过程可能很复杂,如果编译器没有找到与参数类型完全匹配的方法,或者经过类型转换后有多个方法与之匹配,就会报告一个错误
- 如果是private方法、static方法、final方法或者构造器,那么编译器可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。
- 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的哪个类的方法。假设x的实际类型是D,它是C的子类,如果D类定义了方法f(String),就直接调用它,否则将在D类的超类中寻找f(String),以此类推
每次调用方法都需要进行搜索,时间开销相当大,因此虚拟机预先为每个类创建了一个方法表,其中列出了所有方法的签名和实际调用的方法,这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。
阻止继承
不允许扩展的类被称为final类,如果在定义类的时候使用了final修饰符就表明这个类是final类。
将方法或类声明为final主要目的是:确保它们不会在子类中改变语义。
强制类型转换
进行类型转换的唯一原因是:在暂时忽略对象的实际类型之后,使用对象的全部功能
抽象类
如果自下而上在类的继承层次结构中上移,位于上层的类更具有通用性,甚至可能更加抽象。从某种角度看,祖先类更加通用,人们只将它作为派生其他类的基类,而不作为想使用的特定的实例类。
抽象类充当着占位的角色,它们的具体体现在子类中。扩展抽象类可以有两种选择,一种是在子类中定义部分抽象方法或抽象方法也不定义,这样就必须将子类也标记为抽象类;另一种是定义全部的方法,这样一来,子类就不是抽象的了。
类即使不含抽象方法,也可以将类声明为抽象类
抽象类不能被实例化,也就是说,如果将一个类声明为abstract,就不能创建这个类的对象。可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象
Person p = new Student("Yutou", "Econmoics");
这里的p是一个抽象类的Person的变量,Person引用了一个非抽象子类Student的实例。
受保护访问
有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域,需要将这些方法声明为protected
受保护的方法更具有实际意义。如果需要限制某个方法的使用,就可以将它声明为protected
java用于控制可见性的4个访问修饰符:
- 仅对本类可见——private
- 对所有类可见——public
- 对本包和所有子类可见——protected
- 对本包可见——默认,不需要修饰符
Object
Object类是Java中所有类的始祖。
可以使用Object类型的变量引用任何类型的对象,在java中,只有基本类型不是对象
equals方法
Java语言规范要求equals方法具有下面的特性:
- 自反性:对于任何非空引用x,x.euqals(x)应该返回true
- 对称性:对于任何引用x和y,当且仅当y.equals(x)返回true,x.equals(y)也应该返回true
- 传递性:对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(x)返回true,那x.equals(z)也应该返回true
- 一致性:如果x和y的引用对象没有发生变化,反复调用x.equals(y)应该返回同样的结果
- 对于任意非空引用,x.equals(null)应该返回false
如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass进行检测
如果由超类决定相等的概念,那么就可以使用Instanceof进行检测,这样可以在不同子类的对象之间进行相等的比较。
hasCode方法
散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的。如果x和y是两个不同的对象,x.hashCode()与y.hashCode()基本上不会相同
toString方法
在Object中还有一个重要的方法,就说toString方法,它用于返回表示对象值的字符串。
Object类定义了toString方法,用来打印输出对象所属的类名和散列码,数组继承了object类的toString方法,数组类型将按照旧的格式打印,修正的方式是调用静态方法Arrays.toString
泛型数组列表
ArrayList是一个采用类型参数的泛型类,为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面。
下面声明和构造一个保存Employee对象的数组列表:
ArrayList<Employee> staff = new ArrayList<Employee>();
两边都使用类型参数Employee,有些繁琐,在Java7中,可以省去右边的类型参数:
ArrayList<Employee> staff = new ArrayList<>();
这被称为菱形语法,因为<>是菱形,可以结合New操作符使用菱形语法。编译器会检查新值是什么。如果赋值给一个变量,或传递到某个方法,或者从某个方法返回,编译器会检查这个变量、参数或方法的泛型类型,然后将这个类型放在<>中。
访问数组列表元素
既可以灵活地扩展数组,又可以方便地访问数组元素
ArrayList<X> list = new ArrayList<>;
while(...){
x=...;
list.add(x);
}
X[] a = new X[list.size()];
list.toArray(a);
类型化与原始数组列表的兼容性
编译器对类型转换进行检查之后,如果没有发现违反规则的现象,就将所有的类型化数组列表转换成原始ArrayList对象。在程序运行时,所有的数组列表都是一样的,即没有虚拟机的类型参数。
对象包装器与自动装箱
有时,需要将int这样的基本类型转换为对象。所有的基本类型都有一个与之对应的类。例如,Integer类对应基本类型int。通常,这些类称为包装器(wrapper)
这些对象包装器类拥有很鲜明的名字:Integer、Long、Flaot、Double、Short、Byte、Character、Void和Boolean(前6个类派生于公共的超类Number)对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同样,对象包装器类还是final,因此不能定义它们的子类。
假设想定义一个整型数组列表,而尖括号的数据参数不允许是基本类型。这里就用到了Integer对象包装类
JavaSE 5.0 的另一个改进之处是更加便于添加或获得数组元素,下面这个调用
list.add(3);
//将自动变换成
list.add(Integer.valueOf(3));
相反地,当将一个Integer对象赋给一个Int值时,将会自动地拆箱,也就是说,编译器将下列语句:
int n = list.get(i);
//翻译成
int n = list.get(i).intValue();
装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。
参数数量可变的方法
现在的版本提供了可以用可变的参数数量调用的方法(有时称为"变参"方法)
public class PrintStream{
public PrintStream printf(String fmt,Object ... args){
return format(fmt, args);
}
}
这里的省略号…是java代码的一部分,它表明这个方法可以接收任意数量的对象(除fmt参数之外)
用户自己也可以定义可变参数的方法,并将参数指定为任意类型,甚至是基本类型
枚举类
比较两个枚举类型的值时,永远不需要调用equals,而直接使用"=="就可以了
public enum Size{
SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
private String abbreviation;
private Size(String abbreviation){this.abbreviation = abbreviation}
public String getAbbreviation(){return abbreviation;}
}
所有枚举类型都是Enum类的子类,它们继承了这个类的许多方法,其中最有用的一个是toString,这个方法能够返回枚举常量名。
toString的逆方法是静态方法valueOf
每个枚举类型都有一个静态的values方法,它将返回一个包含全部枚举值的数组。
反射
能够分析类的能力的程序称为反射。
Class类
在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个消息跟踪着每个对象所属的类。
可以通过专门的Java类访问这些信息,保存这些信息的类被称为Class。Object类中的getClass()方法将会返回一个Class类型的实例
如果类在一个包里,包的名字也作为类名的一部分,还可以调用静态方法forName获得类名对应的Class对象
虚拟机为每个类型管理一个Class对象,因此,可以利用==运算符实现两个类对象比较的操作
捕获异常
当程序运行过程中发送错误时,就会抛出异常。
异常有两种类型:未检查异常和已检查异常。对于已检查异常,编译器将会检查是否提供了处理器,然而,有很多常见的异常。编译器不会查看是否为这些错误提供了处理器。
利用反射分析类的能力
在java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的域、方法和构造器。这三个类都有一个叫做getName的方法,用来返回项目的名称。Filed类有一个getType方法,用来返回描述域所属类型的Class对象。Method和Constructor类有能够报告参数类型的方,Method类还有一个可以报告返回类型的方法。
这三个类还有一个叫做getModifiers的方法,它将返回一个整型数值,用不同的位开关描述public和static这样的修饰符使用状况。
另外还可以利用java.lang.reflect包中的Modifier类静态方法分析getModifiers返回的整型数值。
在运行时使用反射分析对象
查看对象域的关键方法是Filed类中的get方法。如果f是一个Field类型的对象,obj是某个包含f域的类的对象,f.get(obj)将返回一个对象,其值为obj域的当前值。
反射机制的默认行为受限于java的访问控制。然而,如果一个Java程序没有受到安全管理器的控制,就可以覆盖访问控制。
继承设计的技巧
- 将公共操作和域放在超类
- 不要使用受保护的域
- 使用继承实现"is-a"关系
- 除非所有继承的方法都有意义,否则不要使用继承
- 在覆盖方法时,不要改变预期的行为
- 使用多态,而非类型信息
- 不要过多的使用反射
接口与内部类
接口技术,主要用来描述类具有功能,而并不给出每个功能的具体实现。一个类可以实现一个或多个接口,并且需要接口的地方,随时使用实现了相应接口的对象。
内部类定义在另一个类的内部,其中的方法可以访问包含它们的外部类的域,这是一项比较复杂的技术。内部类技术主要用于设计具有相互协作关系的类集合。
接口
在java程序设计语言中,接口不是类,而是对类的一组需求描述,这个类要遵从接口描述的统一格式进行定义。
接口中的所有方法自动地属于public,因此,在接口中声明方法时,不必提供关键字public。
接口绝不能含有实例域,也不能在接口中实现方法。提供实例域和方法实现的任务应该由实现接口那个类来完成。因此,可以将接口看成是没有实例域的抽象类。但是这个两个概念还是有一定区别的。
为了让类实现一个接口,通常需要下面两个步骤:
- 将类声明为实现给定的接口
- 对接口中的所有方法进行定义
接口的特性
接口不是类,尤其不能用new运算符实例化一个接口
然而,尽管不能构造接口的对象,却能声明接口的变量
接口变量必须引用实现了接口的类对象
接口与抽象类
使用抽象类表示通用属性存在这样一个问题:每个类只能扩展于一个类。
对象克隆
当拷贝一个变量时,原始变量与拷贝变量引用同一个对象,这就是说,改变一个变量所引用的对象将会对另一个变量产生影响。
如果在对象中包含了子对象的引用,拷贝的结构会使得两个域引用同一个子对象,因此原始对象与克隆对象共享这部分信息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CVZEfKTs-1619158617760)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210417212230194.png)]
接口与回调
回调是一种常见的程序设计模式。在这种模式,可以指出某个特定事件发送时应该采取动作。
内部类
内部类是定义在另一个类中的类。使用内部类的原因:
- 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
- 内部类可以对同一个包中的其他类隐藏起来
- 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷
从传统意义上讲,一个方法可以引用调用这个方法的对象数据域。内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。
局部内部类
public void start(){
class TimePrinter implements ActionListner{
public void actionPerformed(ActionEvent event){
Date now = new Date();
System.out.println("At the tone, the time is " + now);
if(beep) Tookit.getDefaultToolkit().beep();
}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(interval, listner);
t.start();
}
局部类不能用public或private访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。
局部类有一个优势,即对外部世界可以完全地隐藏起来。即使TakingClock类中的其他代码也不能访问它。除start方法之外,没有任何方法知道TImePrinter类的存在。
由外部方法访问final变量
与其他内部类相比较,局部类还有一个优点。它们不仅能够访问包含它们的外部类,还可以访问局部变量。不过,那些局部变量必须被声明为final。
匿名内部类
将局部内部类的使用再深入一步。假如只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类。
静态内部类
使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。为此,可以将内部类声明为static,以便取消产生的引用。
代理
利用代理可以在运行时创建一个实现了一组给定接口的新类。这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。
假设有一个表示接口的Class对象,它的确切类型在编译时无法知道。
异常、断言、日志和调试
Java使用一种称为异常处理的错误捕获机制处理。
使用断言
在一个具有自我保护能力的程序中,断言很常用。假设确信某个属性符合要求,并且代码的执行依赖于这个属性。
断言机制运行在测试期间向代码中插入一些检查语句,当代码发布时,这些插入的检测语句将会自动地移走。
assert 条件; 和assert 条件:表达式
这两种形式都会对条件进行检测,如果结构为false,则抛出一个AssertionError异常。在第二种形式中,表达式将被传入AssertionError的构造器,并转换一个消息字符串。
启动和禁用断言
在默认情况下,断言被禁用。可以在运行程序时用下面代码启用它:
java -enableassertions MyApp
需要注意的是,在启用或禁用断言时不必重新编译程序。启用或禁用是类加载器的功能。当断言被禁用时,类加载器将跳过断言代码。因此,不会降低程序运行的速度。
使用断言完成参数检查
在java语言中,给出了3种处理系统错误的机制:
- 抛出一个异常
- 日志
- 使用断言
什么时候应该选择使用断言?
- 断言失败是致命的,不可恢复的错误
- 断言检查只用于开发和测试阶段
记录日志
记录日志API优点:
- 可以很容易地取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭这个操作也很容易
- 可以很简单地禁止日志记录的输出,因此,将这些日志代码留在程序中的开销很小。
- 日志记录可以被定向到不同的处理器,用于在控制台中显示,用于存储在文件中等。
- 日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤实现器指定的标准丢弃那些无用的记录项。
- 日志记录可以采用不同的方式格式化
- 应用程序可以使用多个日志记录器,它们使用类似包名的这种具有层次结构的名字。
- 在默认情况下,日志系统的配置由配置文件控制。如果需要的话,应用程序可以替换这个配置。
基本日志
可以使用System.out替换它,并调用Info方法记录日志信息
泛型程序设计
为什么要使用泛型程序设计
泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用。
定义简单泛型类
一个泛型类就是具有一个或多个类型变量的类
public class Pair<T>{
private T first;
private T second;
public Pair(){
first = null;
second = null;
}
public T getFirst(){return first;}
public T getSecond(){return second;}
public void setFirst(T newValue){first = newValue;}
public void setSecond(T newValue){second = newValue;}
}
Pair类引入了一个类型变量T,用尖括号括起来,放在类名的后面。泛型类可以有多个类型变量。
类型变量使用大写形式,且比较短。在Java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与直的类型。T(需要时话可以用临近的U和S)表示"任意类型"
泛型方法
clasS ArrayAlg{
public static <T> T getMiddle(T... a){
return a[a.length/2];
}
}
这个方法在普通类中定义的,而不是在泛型类中定义的。然而,这是一个泛型方法。泛型方法可以定义在普通类中,也可以定义在泛型类中。
当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:
String middle = ArrayAlg.<String>getMiddle("John","Q.","Public");
在这种情况下,方法调用中可以省略类型参数
泛型代码和虚拟机
虚拟机没有泛型类型对象——所有对象都属于普通类。
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名,擦除类型变量,并转换为限定类型(无限定的变量用Object)
约束和局限性
不能用类型参数代替基本类型。
因此没有Pair,只有Pair。
运行时类型查询只适用于原始类型
虚拟机的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。
不能创建参数化类型的数组
不能实例化参数化类型的数组
不能实例化类型变量
泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用类型变量
不能抛出或捕获泛型类的实例
通配符类型
Pair<? extends Employee>
表示任何泛型Pair类型,它的类型参数是Employee的子类
通配符的超类型限定
超类型限定:? super Manager 这个通配符限制为Manager的所有超类型
无限定通配符
Pair<?>和Pair的本质不同在于:可以用任意Object对象调用原始的Pair类的setObject方法
通配符捕获
通配符捕获只有在有许多限制的情况下才是合法的。编译器必须要能够确信通配符表达的是单个、确定的类型。
反射和泛型
Class类是泛型的。例如,String.class实际上是一个Class类的对象
虚拟机中的泛型类型信息、
Java泛型的卓越特性之一是在虚拟机中泛型类型的擦除。
集合
集合接口
将集合的接口与实现分离
Java集合类库将接口与实现(implementation)分离。
Java类库中的集合接口和迭代器接口
在java类库中,集合类的基本接口是Collector接口,这个接口有两个基本方法:
public interface Collection<E>{
boolean add(E element);
Iterator<E> iteractor();
}
add方法用于向集合中增加元素。如果添加元素确实改变了集合就返回true,如果集合没有发生变化就返回false。
iterator方法用于放回一个实现了Iterator接口的对象,可以使用这个迭代器对象依次访问集合中的元素。
Collection接口扩展了Iterator接口。因此,对于标准类库中的任何集合都可以使用for each循环。
元素被访问的顺序取决集合类型。如果对ArrayList进行迭代,迭代器将从索引0开始,每迭代一次,索引值加1.然而如果访问HashSet中的元素,每个元素将会按照某种随机的次序出现。
由于Collection与Iterator都是泛型接口,可以编写操作任何集合类型的实用方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eiA54w82-1619158617763)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210420171035312.png)]
散列表
散列表为每个对象计算一个整数,称为散列码。散列码是由对象的实例域产生的一个整数。在Java中,散列表用链表数组实现。每个列表被称为桶。要想查找表中对象的位置,就要先计算它的散列表,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。
如果散列表太满,就需要再散列。如果要对散列表再散列,就需要创建一个桶数更多的表,并把所有元素插入这个新表中,然后丢弃原来的表。装填因子决定何时对散列表进行再散列。
散列表可以用于实现几个重要的数据结构。其中最简单的是set类型。set是没有重复元素的元素集合。
树集
树集是一个有序集合。可以以任何顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。将一个元素添加到树中要比添加到散列表中慢,但是,与将元素添加到数组或链表的正确位置上相比还是快很多的。
映射表
映射表是用来存放键/值对。如果提供了键,就能够找到值。
java类库为映射表提供了两个通用的实现:HashMap和TreeMap,这两个类都实现了Map接口。
散列映射表对键进行散列,树映射表用键的整体顺序对元素进行排序,并将其组织成搜索树。散列或比较函数只能作用于键,与键关联的值不能进行散列或比较。
多线程
每一个任务称为一个线程,它是线程控制的简称。可以同时运行一个以上线程的程序称为多线程程序。
中断线程
当对一个线程调用interrupt方法时,线程的终端状态将被置位。这是每一个线程都具有的Boolean标志。每个线程都应该不时地检查这个标志,以判断线程是否被中断。
线程状态
线程可以有如下6种状态:
- New 新创建
- Runable 可运行
- Blocked 被阻塞
- Waiting 等待
- Timed waiting 计时等待
- Terminated 被终止
线程属性
线程优先级
每个线程都有一个优先级,WINDOWS有7个优先级别,在Linux提供的Java虚拟机,线程的优先级被忽略——所有线程具有相同的优先级。
守护线程的唯一用途是为其他线程提供服务。
执行器
构建一个新的线程是有一定代价的,因为涉及与操作系统的交互。如果程序中创建了大量的生命期很短的线程,应该使用线程池。一个线程池中包含许多准备运行的空闲线程,将Runnable对象交给线程池,就会有一个线程调用run方法。当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。
执行器类有许多静态工厂方法用来构建线程池。
交换器
当两个线程在同一个数据缓冲区的两个实例上工作的时候,就可以使用交换器。