3. Java的基本程序设计结构
3.1 一个简单的Java应用程序
- Java区分大小写
- 类名:大写字母开头,每个单词首字母大写
- 源代码文件与公共类名字相同
- 编译得到类字节码文件
- 函数调用:object.method(parameters)
3.2 注释
在Java中,有3种注释方式
//第一种
/*
第二种
*/
/**
第三种
*/
3.3 数据类型
8种基本类型,4种整型,2种浮点类型,1种字符类型,1种布尔类型
3.3.1 整型
类型 | 存储需求 | 取值范围 |
---|---|---|
int | 4字节 | -2^31 ~ 2^31-1 |
short | 2字节 | -2^15 ~ 2^15-1 |
long | 8字节 | -2^63 ~ 2^63-1 |
byte | 1字节 | -128 ~ 127 |
长整型数值有后缀L或l,十六进制数值有前缀0x或0X,八进制有前缀0,二进制有前缀0b或0B。
Java没有任何无符号形式的整型。
3.3.2 浮点类型
类型 | 存储需求 | 取值范围 |
---|---|---|
float | 4字节 | 略 |
double | 8字节 | 略 |
float类型数值有后缀F或f
3.3.3 char类型
在Java中,char类型描述UTF-16一个代码单元,占2字节
3.3.5 boolean类型
两个值:false和true。整型值和布尔值不能相互转换
3.4 变量与常量
3.4.1 声明变量
- 在Java中,每个变量都有一个类型
- 变量名由字母开头并由字母或数字构成的序列。字母包括Unicode字符。变量名大小写敏感,长度没限制
- 不能使用Java保留字作为变量名
3.4.2 变量初始化
- 声明一个变量后必须显示初始化,不能使用未初始化的变量的值
- 在Java中可以将声明放在代码任何位置
- 在Java中,变量的声明尽可能靠近变量第一次使用的地方
3.4.3 常量
-
在Java中,使用final指示常量
-
常量赋值后不能修改,习惯上常量名全大写,用"_"连接
-
在Java中,希望某常量在一个类多个方法使用,称这些常量为类常量。使用static final声明类常量
3.4.4 枚举类型
- 变量取值在有限的集合内,使用枚举
3.5 运算符
3.5.1 算术运算符
- 使用算术运算符"+", “-”, “*”, "/"表示加减乘除运算
- 参与"/"运算的两个数都是整数,表示整数除法;否则,表示浮点除法
- 整数求余用"%"表示
- 整数被0除产生异常,浮点数被0除得到无穷大或NaN
3.5.2 数学函数与常量
- 平方根:Math.sqrt(x)
- 幂运算:Math.pow(x, a)
- 正负整数求余:floorMod
3.5.3 数值类型之间的转换
当一个二元运算符连接两个值时(例如n + f),先将两个操作数转换为同一类型,然后进行计算。
- 如果两个操作数有一个是double类型,另一个操作数就会转换为double类型
- 否则,如果其中一个操作数是float类型,另一个操作数将会转换为float类型
- 否则,如果其中一个操作数是float类型,另一个操作数将会转换为long类型
- 否则,两个操作数都转换为int类型
3.6 字符串
3.6.1 子串
String类的substring方法可以提取子串
String greeting = "Hello";
String s = greeting.substring(0, 3);
substring方法的第二个参数是不想复制的第一个位置。
3.6.2 拼接
使用“+”拼接字符串
当将一个字符串与一个非字符串的值进行拼接时,后者会转换为字符串
int age = 13;
String rating = "PG" + age;
System.out.println(rating); // 输出"PG13"
3.6.3 不可变字符串
String类不提供修改字符串中某个字符串的方法。如果要修改,可以提取要保留的子串,再与希望替换的字符拼接。
greeting = greeting.substring(0, 3) + "p!";
由于不能修改Java字符串的单个字符,所以将String类对象称为不可变(immutable)。不过,可以修改字符串变量,让它引用另一个字符串。
3.6.4 检测字符串是否相等
可以使用equals方法检测两个字符串是否相等。
s.equals(t);
如果字符串s与字符串t相等,返回true;否则,返回false。s与t可以是字符串变量,也可以是字符串字面量。
一定不要使用==运算符检测两个字符串是否相等。这个字符串只能确定两个字符串是否存放在同一个位置。
3.6.5 空串与null串
空串""是长度为0的字符串。
判断:
if(str.length() == 0)
//或
if(str.equals(""))
String变量也可以存放一个特殊值,null,表示目前没有对象与该变量关联。
判断:
if(str == null)
有时要检查一个字符串既不是null也不是空串,使用以下条件:
if(str != null && str.length() != 0)
3.6.6 码点与代码单元
length方法返回采用UTF-16编码表示字符串的代码单元数量。
调用s.charAt(n)将返回位置n的代码单元。
在Java中,char类型描述UTF-16编码的一个代码单元。
3.6.7 String API
-
char charAt(int index)
获取指定位置的字符
-
indexOf(String s)
返回参数s在指定字符串出现的索引位置。如果没有检索到,返回-1
-
substring(int beginIndex, int endIndex)
返回指定位置的子字符串
-
trim()
返回字符串的副本,忽略前导空格和尾部空格
-
replace(String oldChar, String newChar)
将指定字符串或字符替换为新的字符串或字符
-
startsWith(String prefix)
判断字符串前缀是否为指定字符串
-
endsWith(string suffix)
判断字符串后缀是否为指定字符串
-
equals(String otherstr)
判断两个字符串是否相等
-
compareTo(String otherstr)
按字典顺序比较两个字符串,该比较基于字符串每个字符的Unicode值
-
split(String regex)
字符串分割 -
public static String join(CharSequence delimiter, CharSequence… elements)
返回由elements组成的新String,由指定的delimiter连接起来
3.6.9 构建字符串
-
使用StringBuilder构建新的String对象。
-
每次添加内容时,使用append方法
-
调用toString方法得到String对象
StringBuilder builder = new StringBuilder();
builder.append(ch);
builder.append(str);
String completedString = builder.toString();
常用API
-
StringBuilder()
构建空的字符串构建器
-
int length()
-
StringBuilder append(String str)
-
StringBuilder append(char c)
-
StringBuilder insert(int offset, String str)
-
StringBuilder insert(int offset, char c)
-
StringBuilder delete(int startIndex, int endIndex)
-
String toString()
3.7 输入与输出
使用标准输入流读取输入。
Scanner in = new Scanner(System.in);
System.out.println("What is your name?");
String name = in.nextLine();
String firstName = in.next();
System.out.println("How old are you?");
int age = in.nextInt();
//nextDouble
常用API
- Scanner(InputStream in)
- String nextLine()
- String next()
- int nextInt()
- double nextDouble()
- boolean hasNext()
- boolean hasNextInt()
- boolean hasNextDouble()
3.8 控制流程
3.8.5 多重选择:switch语句
case标签可以是:
- 类型为char, byte, short, int的常量表示式
- 枚举常量
- 字符串字面量
3.10 数组
3.10.1 声明数组
数组初始化
int[] smallPrimes = {2, 3, 5, 7, 11, 13};
3.10.3 for each 循环
增强for循环
for (variable : collection) statement
collection 这一集合必须是数组或一个实现Iterable接口的类对象
3.10.4 数组拷贝
将一个数组所有值拷贝到一个新数组,使用Arrays的copyOf方法
int[] copiedLuckyNumbers = Arrays.copyOf(LuckNumbers, LuckyNumbers.length);
3.10.6 数组排序
使用Arrays类的sort方法
int[] a = new int[100];
Arrays.sort(a)
Math.random方法返回[0, 1)范围的数
4. 对象与类
4.1 面向对象程序设计
4.1.1 类
封装:将数据和行为组合在一个对象中,并对对象使用者隐藏具体的实现方式
4.1.4 类之间的关系
- 依赖(“uses a”)
- 聚合(“has a”)
- 继承(“is a”)
4.4 静态字段与静态方法
使用静态方法的情况:
- 方法不需要对象状态
- 方法只需要访问类的静态字段
4.5 方法参数
Java总是按值调用。
4.6 对象构造
4.6.1 重载
多个方法相同名字,不同参数
4.6.3 无参数的构造器
如果写一个类时没有编写构造器,会提供一个无参数构造器,将所有实例字段设置为默认值
4.6.7 初始化块
初始化数据字段的方法:
- 构造器设置值
- 声明中赋值
- 初始化块
调用构造器处理步骤:
- 如果构造器第一行调用另一个构造器,则执行第二个构造器
- 否则,
- 所有数据字段初始化为默认值
- 按照在类声明的顺序,执行所有字段初始化方法和初始化块
- 执行构造器
4.7 包
4.7.1 包名
用因特网域名逆序形式作为包名
4.7.3 在包中增加类
要将类放入包中,必须将包名字放在源文件开头
package com.horstmann.corejava;
...
如果没有放置package语句,这个类属于无名包(unnamed package)
5. 继承
5.1 类、超类、子类
5.1.1 定义子类
使用extends表示继承
5.1.2 覆盖方法
super.getSalary();
调用超类的getSalary方法
5.1.3 子类构造器
public Manager(String name, double salary, int year, int month, int day) {
super(name, salary, year, month, day);
bonus = 0;
}
- 使用super调用超类构造器初始化超类的私有字段。
- 使用super或this调用构造器的语句必须是子类构造器的第一条语句。
- 如果子类构造器没有显式调用构造器,将自动调用超类的无参构造器。
- 如果超类没有无参数的构造器,并且在子类的构造器又没有显式调用其他构造器,编译器会报错。
- 覆盖一个方法的时候,子类方法不能低于超类方法的可见性。
this的两个含义:
- 隐式参数的引用
- 调用该类的其他构造器
super的两个含义:
- 调用超类的方法
- 调用超类的构造器
多态:一个对象变量可以指示多种实际类型的现象
动态绑定:运行时能够自动选择适当的方法
5.1.5 多态
子类引用的数组可以转换为超类引用的数组
//Employee是超类,Manager是子类
Manager[] managers = new Manager[10];
Employee[] staff = managers;
staff[0] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
managers[0].setBonus(5000);//ERROR
将一个普通员工加入经理队列,当调用managers[0].setBonus(5000);
会调用一个不存在的方法,引发ArrayStoreException异常。
为了不发生这类破坏,所有数组都要牢记创建时的元素类型
5.1.6 理解方法调用
方法调用过程:
-
编译器查看对象的声明类型和方法名
-
编译器确定方法调用的参数
-
如果是private方法,static方法,final方法或者构造器,编译器可以确定调用哪个方法,这称为静态绑定。如果调用方法依赖所引用的对象,在运行时进行动态绑定
-
如果采用动态绑定,虚拟机必须调用与所引用对象实际类型对应的方法
- 每次调用方法要搜索调用的方法,时间开销大
- 虚拟机预先为每个类计算了一个方法表,列出所有方法的签名和要调用的实际方法,调用方法的时候,虚拟机查找这个表就行了
在运行时,动态绑定的解析过程:
- 首先,虚拟机获取所引用对象的方法表
- 虚拟机查找所调用的方法
- 虚拟机调用这个方法
5.1.7 阻止继承:final类和方法
final类:不能继承该类,类的方法自动成为final,不包括字段
方法声明final:子类不能覆盖这个方法
5.1.8 类之间的强制类型转换
- 只能在继承层次进行强制类型转换
- 在强制类型转换前,使用instanceof进行检查
- 使用子类特有的方法才需要强制类型转换
5.1.9 抽象类
- 使用抽象类,可以不需要实现一些方法
- 扩展抽象类两种选择:
- 保留抽象类部分或所有抽象方法未定义,子类为抽象类
- 定义所有方法,子类不是抽象类
5.1.10 受保护访问
限制超类的某个方法只允许子类访问,或允许子类方法访问超类某个字段,声明方法或字段为受保护(protected)
4个访问修饰符:
- private:本类可见
- default:本包可见
- protected:本包和所有子类可见
- public:所有类可见
5.2 Object: 所有类的超类
5.2.1 Object类型的变量
- Object类型变量可以引用所有类型对象
- 基本数据类型不是对象
- 所有数组类型都扩展Object类
5.2.2 equals方法
检测一个对象是否等于另外一个对象
5.3 泛型数组列表
5.4 对象包装器与自动装箱
5.5 参数数量可变的方法
5.6 枚举类
枚举的构造器是私有的
5.7 反射
5.7.1 Class类
Java运行时为所有对象维护一个运行时类型标识。
获取:
- obj.getClass()
- Class.ForName(className)
- T.class
6. 接口、lambda表达式与内部类
6.1 接口
6.1.1 接口的概念
- 接口的所有方法都是public方法,接口字段总是public static final
- 实现接口使用关键字implements
- 可以实现多个接口,不能继承多个抽象类
6.1.6 解决默认方法冲突
如果在接口将一个方法定义为默认方法,然后再超类或另一个接口定义同样的方法,如何解决?
- 超类优先。如果超类提供一个具体方法,同名且有相同参数类型的默认方法会被忽略
- 接口冲突。如果一个接口提供默认方法,另一个接口提供一个同名且参数类型相同的方法,必须覆盖这个方法解决冲突
6.1.8 Comparator接口
自定义排序规则
6.2 lambda表达式
6.2.3 函数式接口
只有一个抽象方法的接口
6.2.4 方法引用
方法引用:指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法
三种情况:
- object::instanceMethod
- Class::instanceMethod
- Class::staticMethod
6.3 内部类
- 内部类:定义在另一个类中的类
6.3.1 使用内部类访问对象状态
- 内部类的对象会有一个隐式引用,指向实例化对象的外部类对象。通过这个指针,可以访问外部对象的全部状态
6.3.2 内部类的特殊语法规则
外围类引用
OuterClass.this
6.3.4 局部内部类
- 声明局部类不能有访问说明符(public 或 private)。局部类的作用域被限定在声明这个局部类的块中
- 局部内部类可以访问局部变量
6.3.7 静态内部类
- 可以将内部类声明为static,这样不会生成外围类对象的引用
- 只要内部类不需要访问外围类对象,就应该使用静态内部类
6.5 代理
6.5.1 何时使用代理
代理类:运行时创建类
6.5.2 创建代理对象
使用proxy类的newProxyInstance方法
7. 异常、断言和日志
7.1.1 异常分类
-
所有异常由Throwable继承而来,分为Error类和Exception
-
Error类:系统内部错误
-
Exception类
- RuntimeException:编程错误
- 其他异常:程序没问题,由像IO错误导致的异常
-
非检查型异常:Error类或RuntimeException类
-
检查型错误:其他异常
7.1.2 声明检查型异常
throws Exception
方法必须声明所有可能抛出的检查型异常。
7.1.3 如何抛出异常
throw new EOFException();
7.2 捕捉异常
try/catch
7.2.5 try-with-Resources语句
try (Resource res = ...) {
work with res
}
try块退出时,会调用res.close()
7.4 使用断言
assert condition;
assert condition : expression;
表达式作为消息字符串。
8. 泛型
8.2 泛型类
public class Pair<T>
8.3 泛型方法
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
类型变量放在修饰符(public static)的后面,并在返回类型前面
8.4 类型变量的限定
public static <T extends Comparable> T min(T[] a)...
8.5 泛型代码和虚拟机
8.5.1 类型擦除
- 没有限定的类型变量,替换为Object
public class pair<T> {
private T first;
...
}
替换为
public class Pair {
private Object first;
...
}
- 用第一个限定来替换类型变量
public class Interval <T extends Comparable & Serializable> implements Serializable {
private T lower;
...
}
替换为
public class Interval implements Serializable {
private Comparable lower;
...
}
8.5.2 转换泛型表达式
编写一个泛型方法调用,如果擦除返回类型,编译器会插入强制类型转换
8.5.3 转换泛型方法
class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
...
}
}
class Pair<T> {
private T second;
public void setSecond(T newValue) {
second = newValue;
}
}
类型擦除后
public void setSecond(LocalDate second) {
...
}
与Pair继承的setSecond方法
public void setSecond(Object second) {
...
}
是不同的方法,然而这两个方法不应该不一样。
DateInterval interval = new DateInterval();
Pair<LocalDate> pair = interval;
pair.setSecond(aDate);
我们希望setSecond调用具有多态性。问题在于类型擦除与多态发生冲突。
解决方法:编译器在DateInterval类生成一个桥方法
public void setSecond(Object second) {
setSecond((LocalDate) second);
}
pair引用会调用桥方法,它会调用DateInterval.setSecond(LocalDate)
如果类型变量是返回值,虚拟机会由参数类型和返回类型共同指定一个方法。
总结:
- 虚拟机没有泛型,只有普通类和方法
- 所有类型参数会替换为它们的限定类型或Object类
- 合成桥方法保持多态
- 为保持类型安全性,必要时强制类型转换
8.7 泛型类型的继承规则
Pair< S > 和Pair< T >没有任何关系,通过通配符解决
8.8 通配符类型
泛型类型有各种局限,Java提供一种解决方案:通配符类型
8.8.1 通配符概念
Pair<? extends Employee>
表示任何泛型Pair类型,类型参数是Employee子类
8.8.2 通配符的超类型限定
? super Manager
9. 集合
9.1 Java集合框架
9.1.2 Collection接口
public interface Collection<E> extends Iterable<E> {
boolean add(E element);
Iterator<E> iterator();
}
9.1.3 迭代器
public interface Iterator<E> {
E next();
boolean hasNext();
void remove();
...
}
9.3 具体集合
9.6 算法
9.6.2 排序与混排
List<String> staff = new LinkedList<>();
Collections.sort(staff);
staff.sort(Comparator.comparingDouble(Employee::getSalary).reversed())
9.6.3 二分查找
i = Collections.binarySearch(c, element);
9.6.6 集合与数组的转换
数组转换为集合
String[] values = {"abc", "efg"};
HashSet<String> staff = new HashSet<>(Arrays.asList(values));
集合转换为数组
String[] newvalues = staff.toArray(new String[staff.size()]);
9.7.4 栈
Stack类