Java也不是学得第一个语言了,这里就只记录一些学习过程中发现的之前的没有见过的事情吧
和C
、C++
、golang
对照着学习。
语句
循环 for each
public class Main {
public static void main(String[] args) {
int[] ns = { 1, 4, 9, 16, 25 };
for (int n : ns) {
System.out.println(n);
}
}
}
- 更简单地遍历数组
- 但是无法指定遍历顺序
- 也无法获取数组的索引
- 能够遍历所有“可迭代”的数据类型,包括List、Map等
方法(method)
构造方法
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name) {
this(name, 18); // 调用另一个构造方法Person(String, int)
}
public Person() {
this("Unnamed"); // 调用另一个构造方法Person(String)
}
}
一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。
创建实例时初始化顺序为,先初始化字段,再执行构造方法。
默认构造方法由编译器生成,没有参数,没有执行语句,如果已有自定义,需要重新定义一次默认构造方法。
重载方法
String
类提供了多个重载方法indexOf()
,可以查找子串:
int indexOf(int ch)
:根据字符的Unicode码查找;int indexOf(String str)
:根据字符串查找;int indexOf(int ch, int fromIndex)
:根据字符查找,但指定起始位置;int indexOf(String str, int fromIndex)
根据字符串查找,但指定起始位置。
方法名相同,但各自的参数不同,称为方法重载(Overload
)。
注意:方法重载的返回值类型通常都是相同的。
Arrays类
import java.util.Arrays;
// ns为数组名
// 将一维数组转为字符串
Arrays.toString(ns)
// 将多维数组转为字符串
Arrays.deepToString(ns)
// 数组内容升序排列
Arrays.toString(ns)
继承
protect关键字
class Person {
protected String name;
protected int age;
}
class Student extends Person {
public String hello() {
return "Hello, " + name; // OK!
}
}
子类无法访问父类的private
字段或者private
方法
protected
关键字可以把字段和方法的访问权限控制在继承树内部,一个protected
字段和方法可以被其子类,以及子类的子类所访问
super关键字
public class Main {
public static void main(String[] args) {
Student s = new Student("Xiao Ming", 12, 89);
}
}
class Person {
protected String name;
protected int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(name, age); // 调用父类的构造方法Person(String, int)
this.score = score;
}
}
在Java中,任何class
的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();
如果父类没有默认的构造方法,子类就必须显式调用super()
并给出参数以便让编译器定位到父类的一个合适的构造方法。
子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
转型
Java允许向上转型,抛弃一部分内容,把一个子类型安全地变为更加抽象的父类型。
但是向下转型可能会失败,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来,但是可以把实际是子类的父类向下转型为“子类”
向下转型前要通过instanceof
运算符进行判断,instanceof
实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为null
,那么对任何instanceof
的判断都为false
。
Person p = new Student();
if (p instanceof Student) {
// 只有判断成功才会向下转型:
Student s = (Student) p; // 一定会成功
}
区分组合与继承
子类和父类的关系是is,has关系不能用继承。
多态
覆写Override
在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。
Override和Overload不同的是,如果方法签名如果不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override。方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。在Java程序中,出现这种情况,编译器会报错。
加上@Override可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。
public class Main {
public static void main(String[] args) {
}
}
class Person {
public void run() {}
}
public class Student extends Person {
@Override // Compile error!
public void run(String s) {}
}
多态Polymorphic
运行期才能动态决定调用的子类方法。对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法。
// 多态例子
public class Main {
public static void main(String[] args) {
// 给一个有普通收入、工资收入和享受国务院特殊津贴的小伙伴算税:
Income[] incomes = new Income[] {
new Income(3000),
new Salary(7500),
new StateCouncilSpecialAllowance(15000)
};
System.out.println(totalTax(incomes));
}
public static double totalTax(Income... incomes) {
double total = 0;
for (Income income: incomes) {
total = total + income.getTax();
}
return total;
}
}
class Income {
protected double income;
public Income(double income) {
this.income = income;
}
public double getTax() {
return income * 0.1; // 税率10%
}
}
class Salary extends Income {
public Salary(double income) {
super(income);
}
@Override
public double getTax() {
if (income <= 5000) {
return 0;
}
return (income - 5000) * 0.2;
}
}
class StateCouncilSpecialAllowance extends Income {
public StateCouncilSpecialAllowance(double income) {
super(income);
}
@Override
public double getTax() {
return 0;
}
}
调用super
// 在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。
class Person {
protected String name;
public String hello() {
return "Hello, " + name;
}
}
Student extends Person {
@Override
public String hello() {
// 调用父类的hello()方法:
return super.hello() + "!";
}
}
抽象类
abstract关键字
把一个方法声明为abstract,表示它是一个抽象方法,本身没有实现任何方法语句。由于这个抽象方法本身是无法执行的,所以,抽象方法所在的类也无法被实例化。必须把类本身也声明为abstract,才能正确编译它
public class Main {
public static void main(String[] args) {
Person p = new Student();
p.run();
}
}
abstract class Person {
public abstract void run();
}
class Student extends Person {
@Override
public void run() {
System.out.println("Student.run");
}
}
由抽象类的概念,引申出面向抽象编程的概念,上层代码(抽象类)只定义规范(如run方法必须覆写),具体的业务逻辑由下层代码(具体的类)实现,调用者不必关心。
接口
接口相比于抽象类没有字段,所有方法全部都是抽象方法。接口是比抽象类还要抽象的纯抽象接口。接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。
implement关键字
// 具体的类通过implement关键字实现interface。
class Student implements Person {
private String name;
public Student(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(this.name + " run");
}
@Override
public String getName() {
return this.name;
}
}
接口继承与default方法
interface继承自interface使用extends,它相当于扩展了接口的方法。接口可以继承自多个接口,这与类的继承不同。
interface Person {
String getName();
// 实现类可以不必覆写default方法
default void run() {
System.out.println(getName() + " run");
}
}
class Student implements Person {
private String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
default
方法和抽象类的普通方法是有所不同的。因为interface
没有字段,default
方法无法访问字段,而抽象类的普通方法可以访问实例字段。
接口与抽象类比较
abstract class | interface | |
---|---|---|
继承 | 只能extends一个class | 可以implements多个interface |
字段 | 可以定义实例字段 | 不能定义实例字段 |
抽象方法 | 可以定义抽象方法 | 可以定义抽象方法 |
非抽象方法 | 可以定义非抽象方法 | 可以定义default方法 |
静态字段与静态方法
实例字段
通常从类中定义的字段称为实例字段。每个实例都有独立的字段,各个实例的同名字段互不影响
静态字段
每个实例中的静态字段共享一个空间。静态字段不属于实例,是另存与实例之外的字段。一般不使用实例访问静态字段,而是通过类名访问静态字段
静态方法
调用静态方法不需要实例变量,通过类名就可以调用。类似于其他编程语言的函数。在静态方法内部无法访问this变量,也无法访问其他实例字段,只能访问静态字段。常用于辅助方法,例如Java程序入口的main()
public class Main {
public static void main(String[] args) {
Person.setNumber(99);
System.out.println(Person.number);
}
}
class Person {
public static int number;
public static void setNumber(int value) {
number = value;
}
}
接口静态字段
接口可以有静态字段,但是静态字段必须为final类型。因为接口的字段只能是public static final类型,所以简写也可以。编译器会把通过实例调用的静态方法、静态字段,改写为通过类名调用的。编译器也会把接口中省略的静态子段修饰符自己补上。
public interface Person {
// 编译器会自动加上public statc final:
int MALE = 1;
int FEMALE = 2;
}
包
包名
包是Java中的命名空间,解决名字冲突。需要在定义class的时候第一行申明这个class属于哪个包。没有定义包名的class使用的是默认包,容易引起名字冲突。JVM
执行的时候只区分完整类名:包名.类名.
包名也可以是多层结构,用“ . ”分隔开,推荐使用倒置的域名作为包名。
在电脑中存储的时候,Java文件对应的目录层次要和包的层次保持一致。编译后的class文件也是如此。IDE
会帮助完成这项工作。
同一个包中的类,可以相互访问包作用域下的字段和方法,即不用public / protected / private修饰的字段和方法
import关键字
导入可以是完整包名,也可以通过 * 一次导入该包下所有类(不建议)
还可以通过import static
导入包中类中的静态字段与静态方法(少用)
编译器查找包逻辑
- 如果是完整类名,就直接根据完整类名查找这个class
- 如果是简单类名,按下面的顺序依次查找
- 查找当前
package
是否存在这个class - 查找
import
的包是否包含这个class - 查找
java.lang
包是否包含这个class
- 查找当前
- 如果无法确定,编译报错
默认自动import当前package的其他class;默认自动import java.lang.*
。自动导入的是java.lang
包,但类似java.lang.reflect
这些子包仍需要手动导入。如果有两个class名称相同,例如,mr.jun.Arrays
和java.util.Arrays
,那么只能import其中一个,另一个必须写完整类名。
作用域
public
- 定义为
public的class
、interface
可以被其他任何类访问 - 定义为
public
的field
、method
可以被其他类访问,前提是首先有访问class的权限 - 如果不确定是否需要
public
,就不声明为public
,即尽可能少地暴露对外的字段和方法 - 一个
.java文件
只能包含一个public
类,但可以包含多个非public
类
private
- 定义为private的field、method无法被其他类访问
- private访问权限被限定在class的内部,而且与方法声明顺序无关
推荐把private方法放到后面,因为public方法定义了类对外提供的功能,阅读代码的时候,应该先关注public方法。
// 如果一个类内部还定义了嵌套类(nested class),那么,嵌套类拥有访问private的权限
public class Main {
public static void main(String[] args) {
Inner i = new Inner();
i.hi();
}
// private方法:
private static void hello() {
System.out.println("private hello!");
}
// 静态内部类:
static class Inner {
public void hi() {
Main.hello();
}
}
}
protected
- protected作用于继承关系。定义为protected的字段和方法可以被子类访问,以及子类的子类
package
- 包作用域是指一个类允许访问同一个package的没有public、private修饰的class,以及没有public、protected、private修饰的字段和方法
- 只要在同一个包,就可以访问package权限的class、field和method
- 注意,包名必须完全一致,包没有父子关系,
com.apache
和com.apache.abc
是不同的包 - 把方法定义为package权限有助于测试,因为测试类和被测试类只要位于同一个package,测试代码就可以访问被测试类的package权限方法。
final
- 用final修饰class可以阻止被继承
- 用final修饰method可以阻止被子类覆写
- 用final修饰field可以阻止被重新赋值
- 用final修饰局部变量可以阻止被重新赋值
classpath与jar
JVM
通过环境变量classpath
决定搜索class
的路径和顺序- 不推荐设置系统环境变量
classpath
,始终建议通过-cp
命令传入 - jar包相当于目录,可以包含很多
.class
文件,方便下载和使用 MANIFEST.MF
文件可以提供jar包的信息,如Main-Class
,这样可以直接运行jar包。JVM
自带的标准库rt.jar
不要写到classpath
中,写了反而会干扰JVM
的正常运行。
模块
为了解决jar包之间的依赖问题,Java9后引入了模块的概念。.jmod
文件每一个都是一个模块,模块名就是文件名。例如:模块java.base
对应的文件就是java.base.jmod
。模块之间的依赖关系已经被写入到模块内的module-info.class
文件了。所有的模块都直接或间接地依赖java.base
模块,只有java.base
模块不依赖任何模块,它可以被看作是“根模块”,好比所有的类都是从Object
直接或间接继承而来。
把一堆class封装为jar仅仅是一个打包的过程,而把一堆class封装为模块则不但需要打包,还需要写入依赖关系,并且还可以包含二进制代码(通常是JNI扩展)。此外,模块支持多版本,即在同一个模块中可以为不同的JVM提供不同的版本。
其他具体内容参见廖雪峰教程——模块