基础语法
基本数据结构
Java 的基本数据类型有 8 种,包括 6 种数字类型、1 种字符类型和 1 种布尔类型。
基本数据类型总览
数字类型包括 4 种整数类型和 2 种浮点数类型,4 种整数类型是 byte、short、int 和 long,2 种浮点数类型是 float 和 double。
字符类型是 char,用于表示单个字符。Java 使用统一码对字符进行编码。
布尔类型是 boolean,包括 true 和 false 两种取值。
数字类型直接量
直接量是在程序中直接出现的常量值。
将整数类型的直接量赋值给整数类型的变量时,只要直接量没有超出变量的取值范围,即可直接赋值,如果直接量超出了变量的取值范围,则会导致编译错误。
整数类型的直接量默认是 int 类型,如果直接量超出了 int 类型的取值范围,则必须在其后面加上字母 L 或 l,将直接量显性声明为 long 类型,否则会导致编译错误。
浮点类型的直接量默认是 double 类型,如果要将直接量表示成 float 类型,则必须在其后面加上字母 F 或 f。将 double 类型的直接量赋值给 float 类型的变量是不允许的,会导致编译错误。
基本数据类型之间的转换
有时需要把不同类型的值混合运算,因此需要对数据类型进行转换。
数字类型转换
不同的数字类型对应不同的范围,按照范围从小到大的顺序依次是:byte、short、int、long、float、double。
将小范围类型的变量转换为大范围类型称为拓宽类型,不需要显性声明类型转换。将大范围类型的变量转换为小范围类型称为缩窄类型,必须显性声明类型转换,否则会导致编译错误。
字符类型与数字类型之间的转换
字符类型与数字类型之间可以进行转换。
将数字类型转换成字符类型时,只使用整数的低 16 位(浮点数类型将整数部分转换成字符类型)。
将字符类型转换成数字类型时,字符的统一码转换成指定的数值类型。如果字符的统一码超出了转换成的数值类型的取值范围,则必须显性声明类型转换。
布尔类型不能与其他基本数据类型进行转换
布尔类型不能转换成其他基本数据类型,其他基本数据类型也不能转换成布尔类型。
方法
Java 中的方法,在其他语言中也可能被称为过程或函数,是为执行一个操作而组合在一起的语句组。如果一个操作会被多次执行,则可以将该操作定义成一个方法,执行该操作的时候调用方法即可。
方法的语法结构
方法包括方法头和方法体,方法头又可以分成修饰符、返回值类型、方法名和参数列表,因此方法包括 5 个部分。
- 修饰符:修饰符是可选的,告诉编译器如何调用该方法。
- 返回值类型:方法可以返回一个值,此时返回值类型是方法要返回的值的数据类型。方法也可以没有返回值,此时返回值类型是 void。
- 方法名:方法的实际名称。
- 参数列表:定义在方法头中的变量称为形式参数或参数,简称形参。当调用方法时,需要给参数传递一个值,称为实际参数,简称实参。参数列表指明方法中的参数类型、次序和数量。参数是可选的,方法可以不包含参数。
- 方法体:方法体包含具体的语句集合。
方法名和参数表共同构成方法签名。
参数的值传递
调用方法时,需要提供实参,实参必须与形参的次序相同,称为参数顺序匹配。实参必须与方法签名中的形参在次序上和数量上匹配,在类型上兼容,兼容的意思是不需要显性声明类型转换,即类型相同或者类型转换为拓宽类型。
在调用带参数的方法时,实参的值赋给形参,称为值传递。Java 中只有值传递,无论形参在方法中如何改变,实参不受影响。
- 当参数类型是基本数据类型时,传递的是实参的值,因此不能对实参进行修改。
- 当参数类型是对象时,传递的是对象的引用,此时可以对实参引用的对象进行修改,但是不能让实参引用新的对象。
方法的重载
方法的重载是指在同一个类中的多个方法有相同的名称,但是方法签名不同,编译器能够根据方法签名决定调用哪个方法。由于方法签名由方法名和参数表共同构成,因此方法的重载等同于多个方法有相同的名称和不同的参数列表。
方法的重载可以增加程序的可读性,执行相似操作的方法应该有相同的名称。
关于方法的重载,需要注意以下两点。
- 方法签名只由方法名和参数列表共同构成,因此被重载的方法必须具有不同的参数列表,而不能通过不同的修饰符和返回值类型进行方法的重载。
- 如果一个方法调用有多个可能的匹配,则编译器会调用最合适的匹配方法,如果编译器无法判断哪个方法最匹配,则称为歧义调用,会导致编译错误。
下面用两段示例代码说明方法的重载。
public class Main {
public static void main(String[] args) {
getSum(1, 2);
getSum(1.5, 2.5);
getSum(5, 5.5);
}
public static void getSum(int num1, int num2) {
System.out.println(num1 + "+" + num2 + "=" + (num1 + num2));
}
public static void getSum(double num1, double num2) {
System.out.println(num1 + "+" + num2 + "=" + (num1 + num2));
}
}
示例 2
public class Main {
public static void main(String[] args) {
getSum(1, 2);// 歧义调用,编译错误
}
public static void getSum(int num1, double num2) {
System.out.println(num1 + "+" + num2 + "=" + (num1 + num2));
}
public static void getSum(double num1, int num2) {
System.out.println(num1 + "+" + num2 + "=" + (num1 + num2));
}
}
在示例 1 中,getSum(1, 2) 调用的是参数为两个 int 型的方法,getSum(1.5, 2.5) 和 getSum(5, 5.5) 调用的是参数为两个 double 型的方法,因此运行上述代码得到的输出结果是:
1+2=3
1.5+2.5=4.0
5.0+5.5=10.5
在示例 2 中,getSum(1, 2) 可以同时匹配两个方法,任何一个方法都不比另一个方法更匹配,因此为歧义调用,导致编译错误。
递归
程序调用自身的编程技巧称为递归。递归方法是直接或间接调用自身的方法。
递归的要点
定义递归方法时,需要定义递归的初始状态、初始状态的处理和递归调用。
初始状态也称为终止条件,即最简单的情况,此时应该直接给出如何处理初始状态。
对于非初始状态,则需要进行递归调用,对子问题进行求解,直到初始状态,然后将结果返回给调用者,直到传回原始的调用者。
递归必须定义初始状态,且保证所有的递归调用都能到达初始状态,否则会发生无限递归,导致栈溢出。
递归的优点
递归的优点是代码简洁且易于理解。如果问题满足递归的特点,即可以分解成子问题且子问题与原始问题相似,则可以使用递归给出自然、直接、简单的解法。
递归的缺点
时间和空间的消耗比较大。每一次函数调用都需要在内存栈中分配空间,对栈的操作还需要时间,因此时间复杂度和空间复杂度都会比较高。
如果子问题之间存在重叠,则在不加记忆化的情况下,递归会产生重复计算,导致时间复杂度过高。
由于栈的空间有限,如果递归调用的次数太多,则可能导致调用栈溢出。
尾递归
当递归调用是方法中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。
尾递归的特点是在返回时直接传回原始的调用者,而不用经过中间的调用者,这个特点很重要,因为大多数现代的编译器会利用该特点自动生成优化的代码。
使用尾递归代替普通的递归,可以在时间和空间方面都带来显著的提升。
示例代码
以下代码是计算斐波那契数的普通递归和尾递归的实现。
使用普通递归,会产生大量重复计算,导致时间复杂度过高。
使用尾递归,则不会有重复计算。
public class Fibonacci {
public static long fibonacci(long index) {
if (index <= 1) {
return index;
} else {
return fibonacci(index - 1) + fibonacci(index - 2);
}
}
public static long fibonacciTailRecursion(long index) {
return fibonacciTailRecursion(index, 0, 1);
}
public static long fibonacciTailRecursion(long index, int curr, int next) {
if (index == 0) {
return curr;
} else {
return fibonacciTailRecursion(index - 1, next, curr + next);
}
}
}
面向对象
面向对象的概念
面向对象和面向过程的区别
面向过程:将问题分解成步骤,然后按照步骤实现函数,执行时依次调用函数。数据和对数据的操作是分离的。
面向对象:将问题分解成对象,描述事物在解决问题的步骤中的行为。对象与属性和行为是关联的。
面向过程的优点是性能比面向对象高,不需要面向对象的实例化;缺点是不容易维护、复用和扩展。
面向对象的优点是具有封装、继承、多态的特性,因而容易维护、复用和扩展,可以设计出低耦合的系统;缺点是由于需要实例化对象,因此性能比面向过程低。
对象和类
对象是现实世界中可以明确标识的实体,对象有自己独有的状态和行为。对象的状态由数据域的集合构成,对象的行为由方法的集合构成。
类是定义同一类型对象的结构,是对具有相同特征的对象的抽象。类是一个模板,用来定义对象的数据域和方法。可以从一个类创建多个对象,创建对象称为实例化。
构造方法
构造方法是一种特殊的方法,调用构造方法可以创建新对象。构造方法可以执行任何操作,实际应用中,构造方法一般用于初始化操作,例如初始化对象的数据域。
定义和调用构造方法
构造方法的名称必须和构造方法所在类的名称相同。构造方法可以被重载,即允许在同一个类中定义多个参数列表不同的构造方法。
使用 new 操作符调用构造方法,通过调用构造方法创建对象。
默认构造方法
类可以不显性声明构造方法。此时类中隐性声明了一个方法体为空的没有参数的构造方法,称为默认构造方法。只有当类中没有显性声明任何构造方法时,才会有默认构造方法。
构造方法与普通方法的区别
构造方法与普通方法有三点区别。
构造方法的名称必须与所在的类的名称相同。
构造方法没有返回类型,包括没有 void。
构造方法通过 new 操作符调用,通过调用构造方法创建对象。
示例代码
以下代码定义了一个类 Square,该类描述正方形。
每个正方形都包含边长的数据域,确定边长以后即可确定正方形的大小。
类中有两个构造方法,无参数构造方法创建边长为 1 的正方形对象,有参数构造方法创建指定边长的正方形对象。
类中有两个方法 getPerimeter 和 getArea,分别计算正方形的周长和面积。