目录
面向对象基础
-
class 和 instance
class
是一种对象模版,它定义了如何创建实例,而instance
是对象实例,instance是根据class创建的实例,可以创建多个instance,每个instance类型相同,但各自属性可能不相同 -
定义class 创建一个Person类:
public class Person{
public String name;
public int age;
}
通过class,把一组数据汇集到一个对象上,实现了数据封装
;
class本身是一种数据类型;
-
创建实例
new
一个ming 实例
Person ming=new Person();
ming是实例的变量,可以通过变量访问封装的内容;
方法
-
private 字段用方法访问; 避免外部代码直接访问
field
虽然外部代码不能直接修改private字段,但是,外部代码可以调用方法setName()和setAge()来间接修改private字段。在方法内部,我们就有机会检查参数对不对。比如,setAge()就会检查传入的参数,参数超出了范围,直接报错。这样,外部代码就没有任何机会把age设置成不合理的值。
一个类通过定义方法,就可以给外部代码暴露一些操作的接口,同时,内部自己保证逻辑一致性。
调用方法的语法是实例变量.方法名(参数);。一个方法调用就是一个语句,所以不要忘了在末尾加;。
-
定义方法
修饰符 方法返回类型 方法名(方法参数列表){
若干方法语句;
return 方法返回值;
}
若没有返回值,返回类型设置为void
;
-
private方法 外部代码无法调用,只能在一个类内调用;
-
this变量 在方法内部可以使用
this
,始终指向当前实例。因此,通过this.field
就可以访问当前字段;
若有局部变量和字段名重名,南无局部变量优先级更高,必须加上this;
-
方法参数 方法可以包含0个或人一个参数。方法参数用于接收传递给方法的变量值。调用方法时,必须严格按照参数的定义一一传递。
class Person {
...
public void setNameAndAge(String name, int age) {
...
}
}
调用这个setNameAndAge()
方法时,必须有两个参数,且第一个参数必须为String
,第二个参数必须为int
:
Person ming = new Person();
ming.setNameAndAge("Xiao Ming"); // 编译错误:参数个数不对
ming.setNameAndAge(12, "Xiao Ming"); // 编译错误:参数类型不对
-
可变参数 可变参数用类型...定义,可变参数相当于数组类型:
class Group {
private String[] names;
public void setNames(String... names) {
this.names = names;
}
}
上面的setNames()就定义了一个可变参数。调用时,可以这么写:
Group g = new Group();
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun"); // 传入3个String
g.setNames("Xiao Ming", "Xiao Hong"); // 传入2个String
g.setNames("Xiao Ming"); // 传入1个String
g.setNames(); // 传入0个String
完全可以把可变参数改写为String[]类型:
class Group {
private String[] names;
public void setNames(String[] names) {
this.names = names;
}
}
但是,调用方需要自己先构造String[],比较麻烦。
Group g = new Group();
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]
另一个问题是,调用方可以传入null:
Group g = new Group();
g.setNames(null);
而可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null。
-
参数绑定 基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
public class Main {
public static void main(String[] args) {
Person p = new Person();
String bob = "Bob";
p.setName(bob); // 传入bob变量
System.out.println(p.getName()); // "Bob"
bob = "Alice"; // bob改名为Alice
System.out.println(p.getName()); // "Bob"还是"Alice"?
}
}
class Person {
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
结果:
Bob
Bob
构造方法
在创建对象实例时就把内部字段全部初始化为合适的值
public class Main {
public static void main(String[] args) {
Person p = new Person("Xiao Ming", 15);//调用构造方法
System.out.println(p.getName());
System.out.println(p.getAge());
}
}
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;
}
public int getAge() {
return this.age;
}
}
构造方法的方法名就是类名,没有返回语句;
-
默认构造方法 所有
class
都有构造方法,如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,它没有参数,也没有执行语句,类似这样:
class Person{
public Person(){
}
}
当定义了一个有参的构造方法,默认的构造方法不会保留,此时若调用无参的构造方法就会编译错误
若在定义有参的构造方法后想调用默认构造方法,需要再定义一个无参的构造方法;
-
多构造方法 可以定义多个构造方法,在通过new操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分:
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() {
}
}
如果调用new Person("Xiao Ming", 20)
;,会自动匹配到构造方法public Person(String, int)
。
如果调用new Person("Xiao Ming")
;,会自动匹配到构造方法public Person(String)
。
如果调用new Person()
;,会自动匹配到构造方法public Person()
。
一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是this(…)
:
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)
}
}
方法的重载
在一个类中,我们可以定义多个方法。如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。
这种方法名相同,但各自的参数不同,包括类型及个数,称为方法重载(Overload)
注意:方法重载的返回值类型通常都是相同的。因此不能通过定义不同的返回值类型来定义重载方法。
构造方法的多参数就是一种重载;
继承
类与类之间的关系,关键字extends
称子类继承父类;
Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。
-
protected 继承有个特点,就是子类无法访问父类的
private
字段或者private
方法。例如,Student类就无法访问Person类的name和age字段:
class Person {
private String name;
private int age;
}
class Student extends Person {
public String hello() {
return "Hello, " + name; // 编译错误:无法访问name字段
}
}
这使得继承的作用被削弱了。为了让子类可以访问父类的字段,我们需要把private改为protected
。用protected修饰的字段可以被子类访问:
class Person {
protected String name;
protected int age;
}
class Student extends Person {
public String hello() {
return "Hello, " + name; // OK!
}
}
因此,protected
关键字可以把字段和方法的访问权限控制在继承树内部,一个protected字段和方法可以被其子类,以及子类的子类所访问 ;
-
super
super
关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName
。
在某些时候,就必须使用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) {
this.score = score;
}
}
运行上面的代码,会得到一个编译错误,大意是在Student的构造方法中,无法调用Person的构造方法。
这是因为在Java中,任何class
的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super()
;,所以,Student类的构造方法实际上是这样:
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(); // 自动调用父类的构造方法
this.score = score;
}
}
如果父类没有默认的构造方法,子类就必须显式调用super()
并给出参数以便让编译器定位到父类的一个合适的构造方法。
调用显示的super()方法后不会再调用父类默认的构造方法;
父类没有空参构造,而子类在构造函数的第一句又会默认调用父类的空参构造即隐身的super(),则此时编译不能通过。
这里还顺带引出了另一个问题:即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
-
向上转型 即再Student extends Person 的情况下,下列代码可以实现。
Person s=new Student();
在多态中会有更具体的体现
-
区分继承和组合 若有代码 Book extends Student,这是不符合逻辑的。
因为Student是Person的一种,它们是is关系,而Student并不是Book。实际上Student和Book的关系是has关系。
具有has关系不应该使用继承,而是使用组合,即Student可以持有一个Book实例:
class Student extends Person{
protected Book book;
protected int score;
}
多态
在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)或重写。
Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。
-
多态 举例:
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;
}
}
观察totalTax()方法:利用多态,totalTax()方法只需要和Income打交道,它完全不需要知道Salary和StateCouncilSpecialAllowance的存在,就可以正确计算出总的税。如果我们要新增一种稿费收入,只需要从Income派生,然后正确覆写getTax()方法就可以。把新的类型传入totalTax(),不需要修改任何代码。
多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。
-
覆写Object方法 因为所有的class最终都继承自Object,而Object定义了几个重要的方法:
1.toString():把instance输出为String;
2.equals():判断两个instance是否逻辑相等;
3.hashCode():计算一个instance的哈希值。 -
调用super 在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。
-
final 继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final。用final修饰的方法不能被Override:
class Person {
protected String name;
public final String hello() {
return "Hello, " + name;
}
}
Student extends Person {
// compile error: 不允许覆写
@Override
public String hello() {
}
}
如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final。用final修饰的类不能被继承:
final class Person {
protected String name;
}
// compile error: 不允许继承自Person
Student extends Person {
}
对于一个类的实例字段,同样可以用final修饰。用final修饰的字段在初始化后不能被修改。
class Person {
public final String name = "Unamed";
}
可以在构造方法中初始化final字段:
class Person {
public final String name;
public Person(String name) {
this.name = name;
}
}
这种方法更为常用,因为可以保证实例一旦创建,其final字段就不可修改。
-
练习 给一个有工资收入和稿费收入的小伙伴算税。
参考:
public class Main{
public static void main(String[] args){
Income[] incomes=new Income[]{
new Salary(7500),new Gaofee(1200)
};
System.out.println(Tax(incomes));
}
public static double Tax(Income... incomes){
double totle=0;
for(Income income:incomes){
totle+=income.getTax();
}
return totle;
}
}
class Income{
protected double incomes;
public Income(){}
public Income(double incomes){
this.incomes=incomes;
}
public double getTax(){
return incomes*0.1;
}
}
class Salary extends Income{
public Salary(double incomes){
this.incomes=incomes;
}
public double getTax(){
if(incomes>5000){
return (incomes-5000)*0.2;
}else return 0;
}
}
class Gaofee extends Income{
public Gaofee(double incomes){
this.incomes=incomes;
}
public double getTax(){
return incomes*0.1;
}
}
抽象类
如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法:
abstract class Person{
public abstract void run();
}
抽象类中可以没有抽象方法,但是抽象方法一定在抽象类中。 因为抽象方法和抽象类一样,不能被实例化。
-
抽象类 抽象类无法实例化
Person p = new Person(); // 编译错误
因为抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法
即若父类是抽象类,则父类的抽象方法必须重写;
-
面向抽象编程 当我们定义了抽象类Person,以及具体的Student、Teacher子类的时候,我们可以通过抽象类Person类型去引用具体的子类的实例:
Person s=new Student();
Person t=new Teacher();
-
练习 用抽象类给一个有工资收入和稿费收入的小伙伴算税。
接口
在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。
如果一个抽象类没有字段,所有方法全部都是抽象方法:
abstract class Person{
public abstract void run();
public abstract String getName();
}
就可以把该抽象类改写为接口:interface。
interface Person{
void run;
String getName();
}
所谓interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。
当一个具体的class去实现一个interface时,需要使用implements关键字。举个例子:
class Student implements Person {
private String name;
public Student(String name) {
this.name = name;
}
public void run() {
System.out.println(this.name + " run");
}
public String getName() {
return this.name;
}
}
在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface
class Student implements Person,Hello{
}
-
接口继承 一个interface可以继承自另一个interface。interface继承自interface使用extends,它相当于扩展了接口的方法。例如:
interface Hello {
void hello();
}
interface Person extends Hello {
void run();
String getName();
}
Person接口继承自Hello接口,因此,Person接口现在实际上有3个抽象方法签名,其中一个来自继承的Hello接口。
-
default方法 实现类可以不必覆写default方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。
-
练习 用接口给一个有工资收入和稿费收入的小伙伴算税。
静态字段和静态方法
在一个class中定义的字段,我们称之为实例字段。用static修饰的字段,称为静态字段:static field。
实例字段在每个实例中都有自己的一个独立“空间”,但是静态字段只有一个共享“空间”,所有实例都会共享该字段。举个例子:
class Person{
pbulic String name;
public int age;
public static int number;
}
无论Person 实例的对象是谁,如number赋值则所有对象的number的值都一样;
因此,不推荐用实例变量.静态字段去访问静态字段,因为在Java程序中,实例对象并没有静态字段。在代码中,实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为类名.静态字段来访问静态对象。
推荐用类名来访问静态字段。可以把静态字段理解为描述class本身的字段(非实例字段)。
-
静态方法 因为静态方法属于class而不属于实例,因此,静态方法内部,无法访问this变量,也无法访问实例字段,它只能访问静态字段。
静态方法经常用于工具类。例如:
-
Arrays.sort()
-
Math.random() 静态方法也经常用于辅助方法。注意到Java程序的入口main()也是静态方法。
-
接口的静态字段 因为interface是一个纯抽象类,所以它不能定义实例字段。但是,interface是可以有静态字段的,并且静态字段必须为final类型:
public interface Person {
public static final int MALE = 1;
public static final int FEMALE = 2;
}
实际上,因为interface的字段只能是public static final类型,所以我们可以把这些修饰符都去掉,上述代码可以简写为:
public interface Person{
int MALE=1;
int FAMALE=2;
}
编译器会自动把该字段变为public static final类型。
-
练习 给Person类增加一个静态字段count和静态方法getCount,统计实例创建的个数。
包
-
包作用域 Person类定义在hello包下面:
package hello;
public class Person {
// 包作用域:
void hello() {
System.out.println("Hello!");
}
}
Main类也定义在hello包下面:
package hello;
public class Main {
public static void main(String[] args) {
Person p = new Person();
p.hello(); // 可以调用,因为Main和Person在同一个包
}
}
-
import 在一个class中,我们总会引用其他的class。例如,小明的ming.Person类,如果要引用小军的mr.jun.Arrays类,他有三种写法:
第一种,直接写出完整类名,例如:
// Person.java
package ming;
public class Person {
public void run() {
mr.jun.Arrays arrays = new mr.jun.Arrays();
}
}
第二种写法是用import语句,导入小军的Arrays,然后写简单类名:
// Person.java
package ming;
// 导入完整类名:
import mr.jun.Arrays;
public class Person {
public void run() {
Arrays arrays = new Arrays();
}
}
在写import的时候,可以使用*,表示把这个包下面的所有class都导入进来(但不包括子包的class):
// Person.java
package ming;
// 导入mr.jun包的所有class:
import mr.jun.*;
public class Person {
public void run() {
Arrays arrays = new Arrays();
}
}
还有一种import static的语法,它可以导入可以导入一个类的静态字段和静态方法:
package main;
// 导入System类的所有静态字段和静态方法:
import static java.lang.System.*;
public class Main {
public static void main(String[] args) {
// 相当于调用System.out.println(…)
out.println("Hello, world!");
}
}
import static很少使用。
作用域
-
public 定义为public的class、interface可以被其他任意类访问;
-
private private访问权限被限定在class内部,且与方法的声明顺序无关。 由于Java支持嵌套类,如果一个类内部还定义了嵌套类,那么,嵌套类拥有访问private的权限:
public class Main(){
public static void main(String[] args){
Inner i=new Inner();
i.hi();
}
private static void hello(){
System.out.println("hello");
}
static class Inner{
public void hi(){
Main.hello();
}
}
}
定义在一个class内部的class称为嵌套类(nested class)
-
protected protected作用于继承关系。定义为protected的字段和方法可以被子类访问,以及子类的子类
-
package 包作用域是指一个类允许访问同一个package的没有public、private修饰的class,以及没有public、protected、private修饰的字段和方法
注意,包名必须完全一致,包没有父子关系,com.apache和com.apache.abc是不同的包。 -
局部变量 在方法内部定义的变量称为局部变量,局部变量作用域从变量声明处开始到对应的块结束。方法参数也是局部变量。
-
final 用final修饰class可以阻止被继承:
package abc;
// 无法被继承:
public final class Hello {
private int n = 0;
protected void hi(int t) {
long i = t;
}
}
用final修饰method可以阻止被子类覆写:
package abc;
public class Hello {
// 无法被覆写:
protected final void hi() {
}
}
用final修饰field可以阻止被重新赋值:
package abc;
public class Hello {
private final int n = 0;
protected void hi() {
this.n = 1; // error!
}
}
用final修饰局部变量可以阻止被重新赋值:
package abc;
public class Hello {
protected void hi(final int t) {
t = 1; // error!
}
}
内部类
-
Inner Class 如果一个类定义在另一个类的内部,这个类就是Inner Class
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Nested"); // 实例化一个Outer
Outer.Inner inner = outer.new Inner(); // 实例化一个Inner
inner.hello();
}
}
class Outer {
private String name;
Outer(String name) {
this.name = name;
}
class Inner {
void hello() {
System.out.println("Hello, " + Outer.this.name);
}
}
}
观察上述代码,要实例化一个Inner,我们必须首先创建一个Outer的实例,然后,调用Outer实例的new来创建Inner实例:
Outer outer=new Outer();
Outer.Inner inner = outer.new Inner();
这是因为Inner Class除了有一个this指向它自己,还隐含地持有一个Outer Class实例,可以用Outer.this访问这个实例。所以,实例化一个Inner Class不能脱离Outer实例。
Inner Class和普通Class相比,除了能引用Outer实例外,还有一个额外的“特权”,就是可以修改Outer Class的private字段,因为Inner Class的作用域在Outer Class内部,所以能访问Outer Class的private字段和方法。
观察Java编译器编译后的.class文件可以发现,Outer类被编译为Outer.class,而Inner类被编译为Outer$Inner.class。
-
Anonymous Class 它不需要在Outer Class中明确地定义这个Class,而是在方法内部,通过匿名类(Anonymous Class)来定义
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Nested");
outer.asyncHello();
}
}
class Outer {
private String name;
Outer(String name) {
this.name = name;
}
void asyncHello() {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello, " + Outer.this.name);
}
};
new Thread(r).start();
}
}
观察asyncHello()方法,我们在方法内部实例化了一个Runnable。Runnable本身是接口,接口是不能实例化的,所以这里实际上是定义了一个实现了接口的匿名类,并且通过new实例化该匿名类,然后转型为Runnable。在定义匿名类的时候就必须实例化它,定义匿名类的写法如下:
Runnable r = new Runnable() {
// 实现必要的抽象方法...
};
除了接口外,匿名类也完全可以继承自普通类。观察以下代码:
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HashMap<String, String> map1 = new HashMap<>();
HashMap<String, String> map2 = new HashMap<>() {}; // 匿名类!
HashMap<String, String> map3 = new HashMap<>() {
{
put("A", "1");
put("B", "2");
}
};
System.out.println(map3.get("A"));
}
}
map1是一个普通的HashMap实例,但map2是一个匿名类实例,只是该匿名类继承自HashMap。map3也是一个继承自HashMap的匿名类实例,并且添加了static代码块来初始化数据。观察编译输出可发现Main2.class两个匿名类文件。
-
Static Nested Class 和Inner Class类似,但是使用static修饰,称为静态内部类(Static Nested Class):
public class Main {
public static void main(String[] args) {
Outer.StaticNested sn = new Outer.StaticNested();
sn.hello();
}
}
class Outer {
private static String NAME = "OUTER";
private String name;
Outer(String name) {
this.name = name;
}
static class StaticNested {
void hello() {
System.out.println("Hello, " + Outer.NAME);
}
}
}
用static修饰的内部类和Inner Class有很大的不同,它不再依附于Outer的实例,而是一个完全独立的类,因此无法引用Outer.this,但它可以访问Outer的private静态字段和静态方法。如果把StaticNested移到Outer之外,就失去了访问private的权限。