创建类:
1 2 3 4 class Person { public String name; public int age; }
创建实例:
1 Person ming = new Person();
在OOP中,class
和instance
是“模版”和“实例”的关系;定义class
就是定义了一种数据类型,对应的instance
是这种数据类型的实例;
class
定义的field
,在每个instance
都会拥有各自的field
,且互不干扰;访问实例字段的方法是变量名.字段名
;
通过new
操作符创建新的instance
,然后用变量指向它,即可通过变量来引用这个instance
;指向instance
的变量都是引用变量
方法 一个类通过定义方法,就可以给外部代码暴露一些操作的接口,同时,内部自己保证逻辑一致性。
定义方法的语法是:
1 2 3 4 修饰符 方法返回类型 方法名(方法参数列表) { 若干方法语句; return 方法返回值; }
方法分为public
方法,private
方法。内部的方法可以调用private
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Person { private String name; private int birth; public void setBirth (int birth) { this .birth = birth; } public int getAge () { return calcAge(2019 ); } private int calcAge (int currentYear) { return currentYear - this .birth; } }
this 变量 在方法内部,可以使用一个隐含的变量this
,它始终指向当前实例。因此,通过this.field
就可以访问当前实例的字段。
如果没有命名冲突,可以省略this
:
1 2 3 4 5 6 7 class Person { private String name; public String getName () { return name; } }
如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this
方法参数 调用方法时,必须严格按照参数的定义一一传递。
可变参数 :可变参数相当于数组类型:
1 2 3 4 5 6 7 class Group { private String[] names; public void setNames (String... names) { this .names = names; } }
调用时,可以这么写:
1 2 3 Group g = new Group(); g.setNames("Xiao Ming" , "Xiao Hong" , "Xiao Jun" ); g.setNames();
如果把可变参数变成String[]
类型...(String[] names) {}
,调用方需要自己先构造String[]
:
1 2 3 4 g.setNames(new String[] {"Xiao Ming" , "Xiao Hong" , "Xiao Jun" }); g.setNames(null );
参数绑定 :
基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Main { public static void main (String[] args) { Person p = new Person(); int n = 15 ; p.setAge(n); System.out.println(p.getAge()); n = 20 ; System.out.println(p.getAge()); } } class Person { private int age; public int getAge () { return this .age; } public void setAge (int age) { this .age = age; } }
引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Main { public static void main (String[] args) { Person p = new Person(); String[] fullname = new String[] { "Homer" , "Simpson" }; p.setName(fullname); System.out.println(p.getName()); fullname[0 ] = "Bart" ; System.out.println(p.getName()); } } class Person { private String[] name; public String getName () { return this .name[0 ] + " " + this .name[1 ]; } public void setName (String[] name) { this .name = name; } }
注意下例:bob指向的内存改变
1 2 3 4 5 6 Person p = new Person(); String bob = "Bob" ; p.setName(bob); System.out.println(p.getName()); bob = "Alice" ; System.out.println(p.getName());
构造方法 构造方法的名称就是类名。但是,和普通方法相比,构造方法没有返回值(也没有void
),调用构造方法,必须用new
操作符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Main { public static void main (String[] args) { Person p = new Person("Xiao Ming" , 15 ); System.out.println(p.getName()); } } class Person { private String name; private int age; public Person (String name, int age) { this .name = name; this .age = age; } public String getName () { return this .name; } }
默认构造方法 如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,没有参数也没有执行语句。
如果既要能使用带参数的构造方法,又想保留不带参数的构造方法,那么只能把两个构造方法都定义出来:
1 2 3 4 5 6 7 public Person () {} public Person (String name, int age) { this .name = name; this .age = age; }
没有在构造方法中初始化字段时,引用类型的字段默认是null
,数值类型的字段用默认值,int
类型默认值是0
,布尔类型默认值是false
创建对象实例的时候,按照如下顺序进行初始化:
先初始化字段,例如,int age = 10;
表示字段初始化为10
,double salary;
表示字段默认初始化为0
,String name;
表示引用类型字段默认初始化为null
;
执行构造方法的代码进行初始化。
多构造方法 可以定义多个构造方法,在通过new
操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 = name; this .age = 12 ; } public Person () { } }
方法重载 在一个类中,如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。这种方法名相同,但各自的参数不同,称为方法重载(Overload
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Hello { public void hello () { System.out.println("Hello, world!" ); } public void hello (String name) { System.out.println("Hello, " + name + "!" ); } public void hello (String name, int age) { if (age < 18 ) { System.out.println("Hi, " + name + "!" ); } else { System.out.println("Hello, " + name + "!" ); } } }
方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此,调用起来更简单。
例:String
类提供了多个重载方法indexOf()
,可以查找子串:
int indexOf(int ch)
:根据字符的Unicode码查找;
int indexOf(String str)
:根据字符串查找;
int indexOf(int ch, int fromIndex)
:根据字符查找,但指定起始位置;
int indexOf(String str, int fromIndex)
根据字符串查找,但指定起始位置。
JavaBean 如果读写方法符合以下这种命名规范:
1 2 3 4 5 6 7 8 9 public Type getXyz () public void setXyz (Type value) public boolean isChild () public void setChild (boolean value)
那么这种class
被称为JavaBean
。我们通常把一组对应的读方法(getter
)和写方法(setter
)称为属性(property
)。getter
和setter
也是一种数据封装的方法。
JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。此外,JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中。
枚举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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import java.beans.*;public class Test_JavaBean { public static void main (String[] args) throws Exception { BeanInfo info = Introspector.getBeanInfo(Person1.class); for (PropertyDescriptor pd : info.getPropertyDescriptors()) { System.out.println(pd.getName()); System.out.println(" " +pd.getReadMethod()); System.out.println(" " +pd.getWriteMethod()); } } } class Person1 { private String name; private int age; public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void steAge (int age) { this .age = age; } }
继承 子类从父类继承的时候,子类就获得了父类的所有功能,我们只需要为子类编写新增的功能。
注意: 子类自动获得了父类的所有字段,严禁定义与父类重名的字段!
Java使用extends
关键字来实现继承 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Person { private String name; private int age; public String getName () {...} public void setName (String name) {...} } class Student extends Person { private int score; public int getScore () { … } public void setScore (int score) { … } }
在OOP的术语中,我们把Person
称为超类(super class)/ 父类(parent class)/ 基类(base class),把Student
称为子类(subclass)/ 扩展类(extended class)。
继承树 1 2 3 4 5 6 7 8 9 10 11 12 13 ┌───────────┐ │ Object │ └───────────┘ ▲ │ ┌───────────┐ │ Person │ └───────────┘ ▲ │ ┌───────────┐ │ Student │ └───────────┘
没有明确写extends
的类,编译器会自动加上extends Object
。所以,任除了Object
的类,都会继承自某个类。并且一个类有且仅有一个父类。
protected 子类无法访问父类的private
字段或者private
方法。而用protected
修饰的字段可以被子类访问。一个protected
字段和方法可以被其子类,以及子类的子类所访问 :
1 2 3 4 5 6 7 8 9 10 class Person { protected String name; protected int age; } class Student extends Person { public String hello () { return "Hello, " + name; } }
super super
关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName
。
在Java中,任何class
的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();
如果父类的构造方法有参数,那么super()
失效。子类就必须显式调用super()
并给出参数以便让编译器定位到父类的一个合适的构造方法。
1 2 3 4 5 6 7 8 class Student extends Person { protected int score; public Student (String name, int age, int score) { super (name, age); this .score = score; } }
即子类不会继承 任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
向上转型 把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。向上转型实际上是把一个子类型安全地变为更加抽象的父类型:
1 2 3 4 5 6 Person p = new Student(); --- Student s = new Student(); Person p = s; Object o1 = p; Object o2 = s;
向下转型 如果把一个父类类型强制转型为子类类型,就是向下转型(downcasting)
1 2 3 4 Person p1 = new Student(); Person p2 = new Person(); Student s1 = (Student) p1; Student s2 = (Student) p2;
Person
类型p1
实际指向Student
实例,Person
类型变量p2
实际指向Person
实例。在向下转型的时候,把p1
转型为Student
会成功,因为p1
确实指向Student
实例,把p2
转型为Student
会失败,因为p2
的实际类型是Person
,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。
因此,向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException
。
instanceof
操作符,可以先判断一个实例究竟是不是某种类型:
1 2 3 4 5 6 7 Person p = new Person(); System.out.println(p instanceof Person); System.out.println(p instanceof Student); Student s = new Student(); System.out.println(s instanceof Person); System.out.println(s instanceof Student);
从Java 14开始,判断instanceof
后,可以直接转型为指定变量,避免再次强制转型。例如,对于以下代码:
1 2 3 4 5 6 7 8 9 10 11 Object obj = "hello" ; if (obj instanceof String) { String s = (String) obj; System.out.println(s.toUpperCase()); } Object obj = "hello" ; if (obj instanceof String s) { System.out.println(s.toUpperCase()); }
使用instanceof variable
这种判断并转型为指定类型变量的语法时,必须打开编译器开关--source 14
和--enable-preview
。
继承 ? 组合 继承是is关系,组合是has关系。 具有has关系不应该使用继承,而是使用组合,即Student
可以持有一个Book
实例:
1 2 3 4 class Student extends Person { protected Book book; protected int score; }
多态 在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)
Override和Overload不同的是,如果方法签名如果不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override
。
加上@Override
可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。但是@Override
不是必需的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Person { public void run () { System.out.println("Person.run" ); } } class Students extends Person { public void run (String s) { … } public int run () { … } @Override public void run () { System.out.println("Student.run" ); } }
多态 Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。即Person p = new Student()
调用方法后是Student.run
,
多态的特性就是,运行期才能动态决定调用的子类方法。对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法。
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 51 52 public class test_polymorphic { 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 (int i = 0 ;i < incomes.length; i++) { Income income = incomes[i]; 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 ; } } 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 ; } }
利用多态,totalTax()
方法只需要和Income
打交道,它完全不需要知道Salary
和StateCouncilSpecialAllowance
的存在,就可以正确计算出总的税。如果我们要新增一种稿费收入,只需要从Income
派生,然后正确覆写getTax()
方法就可以。把新的类型传入totalTax()
,不需要修改任何代码。
覆写Object方法 Object
定义了几个重要的方法:
toString()
:把instance输出为String
;
equals()
:判断两个instance是否逻辑相等;
hashCode()
:计算一个instance的哈希值。
在必要的情况下,我们可以覆写Object
的这几个方法。
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 class Person { ... @Override public String toString () { return "Person:name=" + name; } @Override public boolean equals (Object o) { if (o instanceof Person) { Person p = (Person) o; return this .name.equals(p.name); } return false ; } @Override public int hashCode () { return this .name.hashCode(); } }
super调用 在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super
来调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Person { protected String name; public String hello () { return "Hello, " + name; } } Student extends Person { @Override public String hello () { return super .hello() + "!" ;
final 继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final
。用final
修饰的方法不能被Override
1 2 3 4 5 6 7 8 9 10 11 12 13 class Person { protected String name; public final String hello () { return "Hello, " + name; } } Student extends Person { @Override public String hello () { } }
如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final
。用final
修饰的类不能被继承:
1 2 3 4 5 6 7 final class Person { protected String name; } Student extends Person { }
类的实例字段,也可以用final
修饰, 用final
修饰的字段在初始化后不能被修改 。 对final
字段重新赋值会报错:.
1 2 3 class Person { public final String name = "Unamed" ; }
可以在构造方法中初始化final字段,可以保证实例一旦创建,其final
字段就不可修改。
抽象类 如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法 [ 类本身也需要声明为abstract
] :
1 2 3 abstract class Person { public abstract void run () ; }
使用abstract
修饰的类就是抽象类。我们无法实例化一个抽象类。
面向抽象编程 当我们定义了抽象类Person
,以及具体的Student
等子类的时候,我们可以通过抽象类Person
类型去引用具体的子类的实例:
1 Person s = new Student();
这样,我们可以对其方法进行调用而不关心Person
变量的具体子类型。这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。
面向抽象编程的本质就是:
上层代码只定义规范(例如:abstract class Person
);
不需要子类就可以实现业务逻辑(正常编译);
具体的业务逻辑由不同的子类实现,调用者并不关心。
接口 抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。
如果一个抽象类没有字段,所有方法全部都是抽象方法 ,那么就可以把该抽象类改写成接口interface
1 2 3 4 interface Person { void run () ; String getName () ; }
当一个具体的class
去实现一个interface
时,需要使用implements
关键字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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; } }
一个类不能从多个类继承,但可以实现多个interface
:
1 2 3 class Student implements Person , Hello { ... }
default方法 实现类可以不必覆写default
方法。default
方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default
方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
default
方法和抽象类的普通方法是有所不同的。因为interface
没有字段,default
方法无法访问字段,而抽象类的普通方法可以访问实例字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 interface Person { String getName () ; 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; } }
abstract class
interface
继承
只能extends一个class
可以implements多个interface
字段
可以定义实例字段
不能定义实例字段
抽象方法
可以定义抽象方法
可以定义抽象方法
非抽象方法
可以定义非抽象方法
可以定义default方法
注意区分“接口” :
Java的接口特指interface
的定义,表示一个接口类型和一组方法签名,而编程接口泛指接口规范,如方法签名,数据格式,网络协议等。
接口继承 一个interface
可以继承自另一个interface
。interface
继承自interface
使用extends
,它相当于扩展了接口的方法。
1 2 3 4 5 6 7 8 interface Hello { void hello () ; } interface Person extends Hello { void run () ; String getName () ; }
继承关系 一般来说,公共逻辑适合放在abstract class
中,具体逻辑放到各个子类,而接口层次代表抽象程度。可以参考Java的集合类定义的一组接口、抽象类以及具体子类的继承关系:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ┌───────────────┐ │ Iterable │ └───────────────┘ ▲ ┌───────────────────┐ │ │ Object │ ┌───────────────┐ └───────────────────┘ │ Collection │ ▲ └───────────────┘ │ ▲ ▲ ┌───────────────────┐ │ └──────────│AbstractCollection │ ┌───────────────┐ └───────────────────┘ │ List │ ▲ └───────────────┘ │ ▲ ┌───────────────────┐ └──────────│ AbstractList │ └───────────────────┘ ▲ ▲ │ │ │ │ ┌────────────┐ ┌────────────┐ │ ArrayList │ │ LinkedList │ └────────────┘ └────────────┘
在使用的时候,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象:
1 2 3 List list = new ArrayList(); Collection coll = list; Iterable it = coll;