0%

在 Java 中,this 关键字指的是当前对象(它的方法正在被调用)的引用,能理解吧,各位亲?不理解的话,我们继续往下看。

看完再不明白,你过来捶爆我,我保证不还手,只要不打脸。

01、消除字段歧义

我敢赌一毛钱,所有的读者,不管男女老少,应该都知道这种用法,毕竟写构造方法的时候经常用啊。谁要不知道,过来,我给你发一毛钱红包,只要你脸皮够厚。

1
2
3
4
5
6
7
8
9
public class Writer {
private int age;
private String name;

public Writer(int age, String name) {
this.age = age;
this.name = name;
}
}

Writer 类有两个成员变量,分别是 age 和 name,在使用有参构造函数的时候,如果参数名和成员变量的名字相同,就需要使用 this 关键字消除歧义:this.age 是指成员变量,age 是指构造方法的参数

02、引用类的其他构造方法

当一个类的构造方法有多个,并且它们之间有交集的话,就可以使用 this 关键字来调用不同的构造方法,从而减少代码量。

比如说,在无参构造方法中调用有参构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Writer {
private int age;
private String name;

public Writer(int age, String name) {
this.age = age;
this.name = name;
}

public Writer() {
this(18, "沉默王二");
}
}

也可以在有参构造方法中调用无参构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Writer {
private int age;
private String name;

public Writer(int age, String name) {
this();
this.age = age;
this.name = name;
}

public Writer() {
}
}

需要注意的是,this() 必须是构造方法中的第一条语句,否则就会报错。

03、作为参数传递

在下例中,有一个无参的构造方法,里面调用了 print() 方法,参数只有一个 this 关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ThisTest {
public ThisTest() {
print(this);
}

private void print(ThisTest thisTest) {
System.out.println("print " +thisTest);
}

public static void main(String[] args) {
ThisTest test = new ThisTest();
System.out.println("main " + test);
}
}

来打印看一下结果:

1
2
print com.cmower.baeldung.this1.ThisTest@573fd745
main com.cmower.baeldung.this1.ThisTest@573fd745

从结果中可以看得出来,this 就是我们在 main() 方法中使用 new 关键字创建的 ThisTest 对象。

04、链式调用

学过 JavaScript,或者 jQuery 的读者可能对链式调用比较熟悉,类似于 a.b().c().d(),仿佛能无穷无尽调用下去。

在 Java 中,对应的专有名词叫 Builder 模式,来看一个示例。

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
public class Writer {
private int age;
private String name;
private String bookName;

public Writer(WriterBuilder builder) {
this.age = builder.age;
this.name = builder.name;
this.bookName = builder.bookName;
}

public static class WriterBuilder {
public String bookName;
private int age;
private String name;

public WriterBuilder(int age, String name) {
this.age = age;
this.name = name;
}

public WriterBuilder writeBook(String bookName) {
this.bookName = bookName;
return this;
}

public Writer build() {
return new Writer(this);
}
}
}

Writer 类有三个成员变量,分别是 age、name 和 bookName,还有它们仨对应的一个构造方法,参数是一个内部静态类 WriterBuilder。

内部类 WriterBuilder 也有三个成员变量,和 Writer 类一致,不同的是,WriterBuilder 类的构造方法里面只有 age 和 name 赋值了,另外一个成员变量 bookName 通过单独的方法 writeBook() 来赋值,注意,该方法的返回类型是 WriterBuilder,最后使用 return 返回了 this 关键字。

最后的 build() 方法用来创建一个 Writer 对象,参数为 this 关键字,也就是当前的 WriterBuilder 对象。

这时候,创建 Writer 对象就可以通过链式调用的方式。

1
2
3
Writer writer = new Writer.WriterBuilder(18,"沉默王二")
.writeBook("《Web全栈开发进阶之路》")
.build();

05、在内部类中访问外部类对象

说实话,自从 Java 8 的函数式编程出现后,就很少用到 this 在内部类中访问外部类对象了。来看一个示例:

1
2
3
4
5
6
7
8
9
10
public class ThisInnerTest {
private String name;

class InnerClass {
public InnerClass() {
ThisInnerTest thisInnerTest = ThisInnerTest.this;
String outerName = thisInnerTest.name;
}
}
}

在内部类 InnerClass 的构造方法中,通过外部类.this 可以获取到外部类对象,然后就可以使用外部类的成员变量了,比如说 name。

摘抄自:https://mp.weixin.qq.com/s/ySGMnBLhKtBnztivj_ImEg

在 Java 中,一个类可以继承另外一个类或者实现多个接口,我想这一点,大部分的读者应该都知道了。还有一点,我不确定大家是否知道,就是一个接口也可以继承另外一个接口,就像下面这样:

1
2
public interface OneInterface extends Cloneable {
}

这样做有什么好处呢?我想有一部分读者应该已经猜出来了,就是实现了 OneInterface 接口的类,也可以使用 Object.clone() 方法了。

1
2
3
4
5
6
public class TestInterface implements OneInterface {
public static void main(String[] args) throws CloneNotSupportedException {
TestInterface c1 = new TestInterface();
TestInterface c2 = (TestInterface) c1.clone();
}
}

除此之外,我们还可以在 OneInterface 接口中定义其他一些抽象方法(比如说深拷贝),使该接口拥有 Cloneable 所不具有的功能。

1
2
3
public interface OneInterface extends Cloneable {
void deepClone();
}

看到了吧?这就是继承的好处:子接口拥有了父接口的方法,使得子接口具有了父接口相同的行为;同时,子接口还可以在此基础上自由发挥,添加属于自己的行为。
以上,把“接口”换成“类”,结论同样成立。让我们来定义一个普通的父类 Wanger

1
2
3
4
5
6
7
public class Wanger {
int age;
String name;
void write() {
System.out.println("我写了本《基督山伯爵》");
}
}

然后,我们再来定义一个子类 Wangxiaoer,使用关键字 extends 来继承父类 Wanger:

1
2
3
4
5
6
public class Wangxiaoer extends Wanger{
@Override
void write() {
System.out.println("我写了本《茶花女》");
}
}

我们可以将通用的方法和成员变量放在父类中,达到代码复用的目的;然后将特殊的方法和成员变量放在子类中,除此之外,子类还可以覆盖父类的方法(比如write() 方法)。这样,子类也就焕发出了新的生命力。

Java 只支持单一继承,这一点,我在上一篇接口的文章中已经提到过了。如果一个类在定义的时候没有使用 extends 关键字,那么它隐式地继承了 java.lang.Object 类——在我看来,这恐怕就是 Java 号称万物皆对象的真正原因了。

那究竟子类继承了父类的什么呢?

子类可以继承父类的非 private 成员变量,为了验证这一点,我们来看下面这个示例。

1
2
3
4
5
6
public class Wanger {
String defaultName;
private String privateName;
public String publicName;
protected String protectedName;
}

父类 Wanger 定义了四种类型的成员变量,缺省的 defaultName、私有的 privateName、共有的 publicName、受保护的 protectedName。

在子类 Wangxiaoer 中定义一个测试方法 testVariable()

可以确认,除了私有的 privateName,其他三种类型的成员变量都可以继承到。

同理,子类可以继承父类的非 private 方法,为了验证这一点,我们来看下面这个示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Wanger {
void write() {
}

private void privateWrite() {
}

public void publicWrite() {
}

protected void protectedWrite() {
}
}

父类 Wanger 定义了四种类型的方法,缺省的 write、私有的 privateWrite()、共有的 publicWrite()、受保护的 protectedWrite()。

在子类 Wangxiaoer 中定义一个 main 方法,并使用 new 关键字新建一个子类对象:

可以确认,除了私有的 privateWrite(),其他三种类型的方法都可以继承到。

不过,子类无法继承父类的构造方法。如果父类的构造方法是带有参数的,代码如下所示:

1
2
3
4
5
6
7
8
9
public class Wanger {
int age;
String name;

public Wanger(int age, String name) {
this.age = age;
this.name = name;
}
}

则必须在子类的构造器中显式地通过 super 关键字进行调用,否则编译器将提示以下错误:

修复后的代码如下所示:

1
2
3
4
5
public class Wangxiaoer extends Wanger{
public Wangxiaoer(int age, String name) {
super(age, name);
}
}

is-a 是继承的一个明显特征,就是说子类的对象引用类型可以是一个父类类型。

1
2
3
4
5
public class Wangxiaoer extends Wanger{
public static void main(String[] args) {
Wanger wangxiaoer = new Wangxiaoer();
}
}

同理,子接口的实现类的对象引用类型也可以是一个父接口类型。

1
2
3
4
5
6
7
public interface OneInterface extends Cloneable {
}
public class TestInterface implements OneInterface {
public static void main(String[] args) {
Cloneable c1 = new TestInterface();
}
}

尽管一个类只能继承一个类,但一个类却可以实现多个接口,这一点,我在上一篇文章也提到过了。另外,还有一点我也提到了,就是 Java 8 之后,接口中可以定义 default 方法,这很方便,但也带来了新的问题:

如果一个类实现了多个接口,而这些接口中定义了相同签名的 default 方法,那么这个类就要重写该方法,否则编译无法通过。

FlyInterface 是一个会飞的接口,里面有一个签名为 sleep() 的默认方法:

1
2
3
4
5
6
public interface FlyInterface {
void fly();
default void sleep() {
System.out.println("睡着飞");
}
}

RunInterface 是一个会跑的接口,里面也有一个签名为 sleep() 的默认方法:

1
2
3
4
5
6
public interface RunInterface {
void run();
default void sleep() {
System.out.println("睡着跑");
}
}

Pig 类实现了 FlyInterface 和 RunInterface 两个接口,但这时候编译出错了。

原本,default 方法就是为实现该接口而不覆盖该方法的类提供默认实现的,现在,相同方法签名的 sleep() 方法把编译器搞懵逼了,只能重写了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Pig implements FlyInterface, RunInterface {

@Override
public void fly() {
System.out.println("会飞的猪");
}

@Override
public void sleep() {
System.out.println("只能重写了");
}

@Override
public void run() {
System.out.println("会跑的猪");
}
}

类虽然不能继承多个类,但接口却可以继承多个接口,这一点,我不知道有没有触及到一些读者的知识盲区。

1
2
3
public interface WalkInterface extends FlyInterface,RunInterface{
void walk();
}

摘抄自:https://mp.weixin.qq.com/s/ySGMnBLhKtBnztivj_ImEg

对于面向对象编程来说,抽象是一个极具魅力的特征。如果一个程序员的抽象思维很差,那他在编程中就会遇到很多困难,无法把业务变成具体的代码。在 Java 中,可以通过两种形式来达到抽象的目的,一种是抽象类,另外一种就是接口。

如果你现在就想知道抽象类与接口之间的区别,我可以提前给你说一个:

一个类只能继承一个抽象类,但却可以实现多个接口。
当然了,在没有搞清楚接口到底是什么,它可以做什么之前,这个区别理解起来会有点难度。

01、接口是什么

接口是通过 interface 关键字定义的,它可以包含一些常量和方法,来看下面这个示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Electronic {
// 常量
String LED = "LED";

// 抽象方法
int getElectricityUse();

// 静态方法
static boolean isEnergyEfficient(String electtronicType) {
return electtronicType.equals(LED);
}

// 默认方法
default void printDescription() {
System.out.println("电子");
}
}

1)接口中定义的变量会在编译的时候自动加上 public static final 修饰符,也就是说 LED 变量其实是一个常量。

Java 官方文档上有这样的声明:

Every field declaration in the body of an interface is implicitly public, static, and final.

换句话说,接口可以用来作为常量类使用,还能省略掉 public static final,看似不错的一种选择,对吧?

不过,这种选择并不可取。因为接口的本意是对方法进行抽象,而常量接口会对子类中的变量造成命名空间上的“污染”。

2)没有使用 privatedefault 或者 static 关键字修饰的方法是隐式抽象的,在编译的时候会自动加上 public abstract 修饰符。也就是说 getElectricityUse() 其实是一个抽象方法,没有方法体——这是定义接口的本意。

3)从 Java 8 开始,接口中允许有静态方法,比如说 isEnergyEfficient() 方法。

静态方法无法由(实现了该接口的)类的对象调用,它只能通过接口的名字来调用,比如说 Electronic.isEnergyEfficient(“LED”)。

接口中定义静态方法的目的是为了提供一种简单的机制,使我们不必创建对象就能调用方法,从而提高接口的竞争力。

4)接口中允许定义 default 方法也是从 Java 8 开始的,比如说 printDescription(),它始终由一个代码块组成,为实现该接口而不覆盖该方法的类提供默认实现,也就是说,无法直接使用一个“;”号来结束默认方法——编译器会报错的。

允许在接口中定义默认方法的理由是很充分的,因为一个接口可能有多个实现类,这些类就必须实现接口中定义的抽象类,否则编译器就会报错。假如我们需要在所有的实现类中追加某个具体的方法,在没有 default 方法的帮助下,我们就必须挨个对实现类进行修改。

来看一下 Electronic 接口反编译后的字节码吧,你会发现,接口中定义的所有变量或者方法,都会自动添加上 public 关键字——假如你想知道编译器在背后都默默做了哪些辅助,记住反编译字节码就对了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Electronic
{

public abstract int getElectricityUse();

public static boolean isEnergyEfficient(String electtronicType)
{
return electtronicType.equals("LED");
}

public void printDescription()
{
System.out.println("\u7535\u5B50");
}

public static final String LED = "LED";
}

有些读者可能会问,“二哥,为什么我反编译后的字节码和你的不一样,你用了什么反编译工具?”其实没有什么秘密,微信搜「沉默王二」回复关键字「JAD」就可以免费获取了,超级好用。

02、定义接口的注意事项

由之前的例子我们就可以得出下面这些结论:

  • 接口中允许定义变量
  • 接口中允许定义抽象方法
  • 接口中允许定义静态方法(Java 8 之后)
  • 接口中允许定义默认方法(Java 8 之后)

除此之外,我们还应该知道:

1)接口不允许直接实例化。

需要定义一个类去实现接口,然后再实例化。

1
2
3
4
5
6
7
8
9
10
11
public class Computer implements Electronic {

public static void main(String[] args) {
new Computer();
}

@Override
public int getElectricityUse() {
return 0;
}
}

2)接口可以是空的,既不定义变量,也不定义方法。

1
2
public interface Serializable {
}

Serializable 是最典型的一个空的接口,我之前分享过一篇文章《Java Serializable:明明就一个空的接口嘛》,感兴趣的读者可以去我的个人博客看一看,你就明白了空接口的意义。

http://www.itwanger.com/java/2019/11/14/java-serializable.html

3)不要在定义接口的时候使用 final 关键字,否则会报编译错误,因为接口就是为了让子类实现的,而 final 阻止了这种行为。

4)接口的抽象方法不能是 private、protected 或者 final。

5)接口的变量是隐式 public static final,所以其值无法改变。

03、接口可以做什么

1)使某些实现类具有我们想要的功能,比如说,实现了 Cloneable 接口的类具有拷贝的功能,实现了 Comparable 或者 Comparator 的类具有比较功能。

Cloneable 和 Serializable 一样,都属于标记型接口,它们内部都是空的。实现了 Cloneable 接口的类可以使用 Object.clone() 方法,否则会抛出 CloneNotSupportedException。

1
2
3
4
5
6
7
8
9
10
11
public class CloneableTest implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}

public static void main(String[] args) throws CloneNotSupportedException {
CloneableTest c1 = new CloneableTest();
CloneableTest c2 = (CloneableTest) c1.clone();
}
}

运行后没有报错。现在把 implements Cloneable 去掉。

1
2
3
4
5
6
7
8
9
10
11
12
public class CloneableTest {
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}

public static void main(String[] args) throws CloneNotSupportedException {
CloneableTest c1 = new CloneableTest();
CloneableTest c2 = (CloneableTest) c1.clone();

}
}

运行后抛出 CloneNotSupportedException:

1
2
3
4
Exception in thread "main" java.lang.CloneNotSupportedException: com.cmower.baeldung.interface1.CloneableTest
at java.base/java.lang.Object.clone(Native Method)
at com.cmower.baeldung.interface1.CloneableTest.clone(CloneableTest.java:6)
at com.cmower.baeldung.interface1.CloneableTest.main(CloneableTest.java:11)

至于 Comparable 和 Comparator 的用法,感兴趣的读者可以参照我之前写的另外一篇文章《来吧,一文彻底搞懂Java中的Comparable和Comparator》。

http://www.itwanger.com/java/2020/01/04/java-comparable-comparator.html

2)Java 原则上只支持单一继承,但通过接口可以实现多重继承的目的。

可能有些读者会问,“二哥,为什么 Java 只支持单一继承?”简单来解释一下。

如果有两个类共同继承(extends)一个有特定方法的父类,那么该方法会被两个子类重写。然后,如果你决定同时继承这两个子类,那么在你调用该重写方法时,编译器不能识别你要调用哪个子类的方法。这也正是著名的菱形问题,见下图。

ClassC 同时继承了 ClassA 和 ClassB,ClassC 的对象在调用 ClassA 和 ClassB 中重载的方法时,就不知道该调用 ClassA 的方法,还是 ClassB 的方法。

接口没有这方面的困扰。来定义两个接口,Fly 会飞,Run 会跑。

1
2
3
4
5
6
public interface Fly {
void fly();
}
public interface Run {
void run();
}

然后让一个类同时实现这两个接口。

1
2
3
4
5
6
7
8
9
10
11
public class Pig implements Fly,Run{
@Override
public void fly() {
System.out.println("会飞的猪");
}

@Override
public void run() {
System.out.println("会跑的猪");
}
}

这就在某种形式上达到了多重继承的目的:现实世界里,猪的确只会跑,但在雷军的眼里,站在风口的猪就会飞,这就需要赋予这只猪更多的能力,通过抽象类是无法实现的,只能通过接口。

3)实现多态。

什么是多态呢?通俗的理解,就是同一个事件发生在不同的对象上会产生不同的结果,鼠标左键点击窗口上的 X 号可以关闭窗口,点击超链接却可以打开新的网页。

多态可以通过继承(extends)的关系实现,也可以通过接口的形式实现。来看这样一个例子。

Shape 是表示一个形状。

1
2
3
public interface Shape {
String name();
}

圆是一个形状。

1
2
3
4
5
6
public class Circle implements Shape {
@Override
public String name() {
return "圆";
}
}

正方形也是一个形状。

1
2
3
4
5
6
public class Square implements Shape {
@Override
public String name() {
return "正方形";
}
}

然后来看测试类。

1
2
3
4
5
6
7
8
9
10
List<Shape> shapes = new ArrayList<>();
Shape circleShape = new Circle();
Shape squareShape = new Square();

shapes.add(circleShape);
shapes.add(squareShape);

for (Shape shape : shapes) {
System.out.println(shape.name());
}

多态的存在 3 个前提:

1、要有继承关系,Circle 和 Square 都实现了 Shape 接口
2、子类要重写父类的方法,Circle 和 Square 都重写了 name() 方法
3、父类引用指向子类对象,circleShape 和 squareShape 的类型都为 Shape,但前者指向的是 Circle 对象,后者指向的是 Square 对象。

然后,我们来看一下测试结果:

1
2

正方形

也就意味着,尽管在 for 循环中,shape 的类型都为 Shape,但在调用 name() 方法的时候,它知道 Circle 对象应该调用 Circle 类的 name() 方法,Square 对象应该调用 Square 类的 name() 方法。

04、接口与抽象类的区别

好了,关于接口的一切,你应该都搞清楚了。现在回到读者春夏秋冬的那条留言,“兄弟,说说抽象类和接口之间的区别?”

1)语法层面上

接口中不能有 public 和 protected 修饰的方法,抽象类中可以有。
接口中的变量只能是隐式的常量,抽象类中可以有任意类型的变量。
一个类只能继承一个抽象类,但却可以实现多个接口。
2)设计层面上

抽象类是对类的一种抽象,继承抽象类的类和抽象类本身是一种 is-a 的关系。

接口是对类的某种行为的一种抽象,接口和类之间并没有很强的关联关系,所有的类都可以实现 Serializable 接口,从而具有序列化的功能。

就这么多吧,能说道这份上,我相信面试官就不会为难你了。

摘抄自:https://mp.weixin.qq.com/s/ySGMnBLhKtBnztivj_ImEg

当我们要完成的任务是确定的,但具体的方式需要随后开个会投票的话,Java 的抽象类就派上用场了。这句话怎么理解呢?搬个小板凳坐好,听我来给你讲讲。

01、抽象类的 5 个关键点

1)定义抽象类的时候需要用到关键字 abstract,放在 class 关键字前。

1
2
public abstract class AbstractPlayer {
}

关于抽象类的命名,阿里出品的 Java 开发手册上有强调,抽象类命名要使用 Abstract 或 Base 开头,记住了哦。

2)抽象类不能被实例化,但可以有子类。

尝试通过 new 关键字实例化的话,编译器会报错,提示“类是抽象的,不能实例化”。

通过 extends 关键字可以继承抽象类,继承后,BasketballPlayer 类就是 AbstractPlayer 的子类。

1
2
public class BasketballPlayer extends AbstractPlayer {
}

3)如果一个类定义了一个或多个抽象方法,那么这个类必须是抽象类。

当在一个普通类(没有使用 abstract 关键字修饰)中定义了抽象方法,编译器就会有两处错误提示。

第一处在类级别上,提醒你“这个类必须通过 abstract 关键字定义”,or 的那个信息没必要,见下图。

第二处在方法级别上,提醒你“抽象方法所在的类不是抽象的”,见下图。

4)抽象类可以同时声明抽象方法和具体方法,也可以什么方法都没有,但没必要。就像下面这样:

1
2
3
4
5
6
7
public abstract class AbstractPlayer {
abstract void play();

public void sleep() {
System.out.println("运动员也要休息而不是挑战极限");
}
}

5)抽象类派生的子类必须实现父类中定义的抽象方法。比如说,抽象类中定义了 play() 方法,子类中就必须实现。

1
2
3
4
5
6
public class BasketballPlayer extends AbstractPlayer {
@Override
void play() {
System.out.println("我是张伯伦,篮球场上得过 100 分");
}
}

如果没有实现的话,编译器会提醒你“子类必须实现抽象方法”,见下图。

02、什么时候用抽象类

与抽象类息息相关的还有一个概念,就是接口,我们留到下一篇文章中详细说,因为要说的知识点还是蛮多的。你现在只需要有这样一个概念就好,接口是对行为的抽象,抽象类是对整个类(包含成员变量和行为)进行抽象。

(是不是有点明白又有点不明白,别着急,翘首以盼地等下一篇文章出炉吧)

除了接口之外,还有一个概念就是具体的类,就是不通过 abstract 修饰的普通类,见下面这段代码中的定义。

1
2
3
4
5
public class BasketballPlayer {
public void play() {
System.out.println("我是詹姆斯,现役第一人");
}
}

有接口,有具体类,那什么时候该使用抽象类呢?

1)我们希望一些通用的功能被多个子类复用。比如说,AbstractPlayer 抽象类中有一个普通的方法 sleep(),表明所有运动员都需要休息,那么这个方法就可以被子类复用。

1
2
3
4
5
public abstract class AbstractPlayer {
public void sleep() {
System.out.println("运动员也要休息而不是挑战极限");
}
}

虽然 AbstractPlayer 类可以不是抽象类——把 abstract 修饰符去掉也能满足这种场景。但 AbstractPlayer 类可能还会有一个或者多个抽象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BasketballPlayer 继承了 AbstractPlayer 类,也就拥有了 sleep() 方法。

public class BasketballPlayer extends AbstractPlayer {
}
BasketballPlayer 对象可以直接调用 sleep() 方法:

BasketballPlayer basketballPlayer = new BasketballPlayer();
basketballPlayer.sleep();
FootballPlayer 继承了 AbstractPlayer 类,也就拥有了 sleep() 方法。

public class FootballPlayer extends AbstractPlayer {
}
FootballPlayer 对象也可以直接调用 sleep() 方法:

FootballPlayer footballPlayer = new FootballPlayer();
footballPlayer.sleep();

2)我们需要在抽象类中定义好 API,然后在子类中扩展实现。比如说,AbstractPlayer 抽象类中有一个抽象方法 play(),定义所有运动员都可以从事某项运动,但需要对应子类去扩展实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class AbstractPlayer {
abstract void play();
}
BasketballPlayer 继承了 AbstractPlayer 类,扩展实现了自己的 play() 方法。

public class BasketballPlayer extends AbstractPlayer {
@Override
void play() {
System.out.println("我是张伯伦,我篮球场上得过 100 分,");
}
}
FootballPlayer 继承了 AbstractPlayer 类,扩展实现了自己的 play() 方法。

public class FootballPlayer extends AbstractPlayer {
@Override
void play() {
System.out.println("我是C罗,我能接住任意高度的头球");
}
}

3)如果父类与子类之间的关系符合 is-a 的层次关系,就可以使用抽象类,比如说篮球运动员是运动员,足球运动员是运动员。

03、具体示例

为了进一步展示抽象类的特性,我们再来看一个具体的示例。假设现在有一个文件,里面的内容非常简单——“Hello World”,现在需要有一个读取器将内容读取出来,最好能按照大写的方式,或者小写的方式。

这时候,最好定义一个抽象类,比如说 BaseFileReader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class BaseFileReader {
protected Path filePath;

protected BaseFileReader(Path filePath) {
this.filePath = filePath;
}

public List<String> readFile() throws IOException {
return Files.lines(filePath)
.map(this::mapFileLine).collect(Collectors.toList());
}

protected abstract String mapFileLine(String line);
}

filePath 为文件路径,使用 protected 修饰,表明该成员变量可以在需要时被子类访问。

readFile() 方法用来读取文件,方法体里面调用了抽象方法 mapFileLine()——需要子类扩展实现大小写的方式。

你看,BaseFileReader 设计的就非常合理,并且易于扩展,子类只需要专注于具体的大小写实现方式就可以了。

小写的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class LowercaseFileReader extends BaseFileReader {
protected LowercaseFileReader(Path filePath) {
super(filePath);
}

@Override
protected String mapFileLine(String line) {
return line.toLowerCase();
}
}
大写的方式:

public class UppercaseFileReader extends BaseFileReader {
protected UppercaseFileReader(Path filePath) {
super(filePath);
}

@Override
protected String mapFileLine(String line) {
return line.toUpperCase();
}
}

你看,从文件里面一行一行读取内容的代码被子类复用了——抽象类 BaseFileReader 类中定义的普通方法 readFile()。与此同时,子类只需要专注于自己该做的工作,LowercaseFileReader 以小写的方式读取文件内容,UppercaseFileReader 以大写的方式读取文件内容。

接下来,我们来新建一个测试类 FileReaderTest:

1
2
3
4
5
6
7
8
9
10
public class FileReaderTest {
public static void main(String[] args) throws URISyntaxException, IOException {
URL location = FileReaderTest.class.getClassLoader().getResource("helloworld.txt");
Path path = Paths.get(location.toURI());
BaseFileReader lowercaseFileReader = new LowercaseFileReader(path);
BaseFileReader uppercaseFileReader = new UppercaseFileReader(path);
System.out.println(lowercaseFileReader.readFile());
System.out.println(uppercaseFileReader.readFile());
}
}

项目的 resource 目录下有一个文本文件,名字叫 helloworld.txt。

可以通过 ClassLoader.getResource() 的方式获取到该文件的 URI 路径,然后就可以使用 LowercaseFileReader 和 UppercaseFileReader 两种方式读取到文本内容了。

输出结果如下所示:

1
2
[hello world]
[HELLO WORLD]

摘抄自:https://mp.weixin.qq.com/s/ySGMnBLhKtBnztivj_ImEg

假设现在有一个 Writer 类,它有两个字段,姓名和年纪:

1
2
3
4
5
6
7
8
9
10
11
12
public class Writer {
private String name;
private int age;

@Override
public String toString() {
return "Writer{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

重写了 toString() 方法,用于打印 Writer 类的详情。由于没有构造方法,意味着当我们创建 Writer 对象时,它的字段值并没有初始化:

1
2
Writer writer = new Writer();
System.out.println(writer.toString());

输出结果如下所示:

1
Writer{name='null', age=0}

name 是字符串类型,所以默认值为 null,age 为 int 类型,所以默认值为 0。

让我们为 Writer 类主动加一个无参的构造方法:

1
2
3
4
public Writer() {
this.name = "";
this.age = 0;
}

构造方法也是一个方法,只不过它没有返回值,默认返回创建对象的类型。需要注意的是,当前构造方法没有参数,它被称为无参构造方法。如果我们没有主动创建无参构造方法的话,编译器会隐式地自动添加一个无参的构造方法。这就是为什么,一开始虽然没有构造方法,却可以使用 new Writer() 创建对象的原因,只不过,所有的字段都被初始化成了默认值。

接下来,让我们添加一个有参的构造方法:

1
2
3
4
public Writer(String name, int age) {
this.name = name;
this.age = age;
}

现在,我们创建 Writer 对象的时候就可以通过对字段值初始化值了。

1
2
Writer writer1 = new Writer("沉默王二",18);
System.out.println(writer1.toString());

来看一下打印结果:

1
Writer{name='沉默王二', age=18}

可以根据字段的数量添加不同参数数量的构造方法,比如说,我们可以单独为 name 字段添加一个构造方法:

1
2
3
public Writer(String name) {
this.name = name;
}

为了能够兼顾 age 字段,我们可以通过 this 关键字调用其他的构造方法:

1
2
3
public Writer(String name) {
this(name,18);
}

把作者的年龄都默认初始化为 18。如果需要使用父类的构造方法,还可以使用 super 关键字,手册后面有详细的介绍。

摘抄自:https://mp.weixin.qq.com/s/ySGMnBLhKtBnztivj_ImEg

类和对象是 Java 中最基本的两个概念,可以说撑起了面向对象编程(OOP)的一片天。对象可以是现实中看得见的任何物体(一只特立独行的猪),也可以是想象中的任何虚拟物体(能七十二变的孙悟空),Java 通过类(class)来定义这些物体,有什么状态(通过字段,或者叫成员变量定义,比如说猪的颜色是纯色还是花色),有什么行为(通过方法定义,比如说猪会吃,会睡觉)。

来,让我来定义一个简单的类给你看看。

1
2
3
4
5
6
7
public class Pig {
private String color;

public void eat() {
System.out.println("吃");
}
}

默认情况下,每个 Java 类都会有一个空的构造方法,尽管它在源代码中是缺省的,但却可以通过反编译字节码看到它。

1
2
3
4
5
6
7
8
9
10
public class Pig {
private String color;

public Pig() {
}

public void eat() {
System.out.println("吃");
}
}

没错,就是多出来的那个 public Pig() {},参数是空的,方法体是空的。我们可以通过 new 关键字利用这个构造方法来创建一个对象,代码如下所示:

Pig pig = new Pig();
当然了,我们也可以主动添加带参的构造方法。

1
2
3
4
5
6
7
8
9
10
11
public class Pig {
private String color;

public Pig(String color) {
this.color = color;
}

public void eat() {
System.out.println("吃");
}
}

这时候,再查看反编译后的字节码时,你会发现缺省的无参构造方法消失了——和源代码一模一样。

1
2
3
4
5
6
7
8
9
10
11
public class Pig {
private String color;

public Pig(String color) {
this.color = color;
}

public void eat() {
System.out.println("吃");
}
}

这意味着无法通过 new Pig() 来创建对象了——编译器会提醒你追加参数。

比如说你将代码修改为 new Pig(“纯白色”),或者添加无参的构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Pig {
private String color;

public Pig(String color) {
this.color = color;
}

public Pig() {
}

public void eat() {
System.out.println("吃");
}
}

使用无参构造方法创建的对象状态默认值为 null(color 字符串为引用类型),如果是基本类型的话,默认值为对应基本类型的默认值,比如说 int 为 0,更详细的见下图。

(图片中有一处错误,boolean 的默认值为 false)

接下来,我们来创建多个 Pig 对象,它的颜色各不相同。

1
2
3
4
5
6
7
public class PigTest {
public static void main(String[] args) {
Pig pigNoColor = new Pig();
Pig pigWhite = new Pig("纯白色");
Pig pigBlack = new Pig("纯黑色");
}
}

你看,我们创建了 3 个不同花色的 Pig 对象,全部来自于一个类,由此可见类的重要性,只需要定义一次,就可以多次使用。

那假如我想改变对象的状态呢?该怎么办?目前毫无办法,因为没有任何可以更改状态的方法,直接修改 color 是行不通的,因为它的访问权限修饰符是 private 的。

最好的办法就是为 Pig 类追加 getter/setter 方法,就像下面这样:

1
2
3
4
5
6
7
public String getColor() {
return color;
}

public void setColor(String color) {
this.color = color;
}

通过 setColor() 方法来修改,通过 getColor() 方法获取状态,它们的权限修饰符是 public 的。

1
2
3
Pig pigNoColor = new Pig();
pigNoColor.setColor("花色");
System.out.println(pigNoColor.getColor()); // 花色

为什么要这样设计呢?可以直接将 color 字段的访问权限修饰符换成是 public 的啊,不就和 getter/setter 一样的效果了吗?

因为有些情况,某些字段是不允许被随意修改的,它只有在对象创建的时候初始化一次,比如说猪的年龄,它只能每年长一岁(举个例子),没有月光宝盒让它变回去。

1
2
3
4
5
6
7
8
9
private int age;

public int getAge() {
return age;
}

public void increaseAge() {
this.age++;
}

你看,age 就没有 setter 方法,只有一个每年可以调用一次的 increaseAge() 方法和 getter 方法。如果把 age 的访问权限修饰符更改为 public,age 就完全失去控制了,可以随意将其重置为 0 或者负数。

访问权限修饰符对于 Java 来说,非常重要,目前共有四种:public、private、protected 和 default(缺省)。

一个类只能使用 public 或者 default 修饰,public 修饰的类你之前已经见到过了,现在我来定义一个缺省权限修饰符的类给你欣赏一下。

1
2
class Dog {
}

哈哈,其实也没啥可以欣赏的。缺省意味着这个类可以被同一个包下的其他类进行访问;而 public 意味着这个类可以被所有包下的类进行访问。

假如硬要通过 private 和 protected 来修饰类的话,编译器会生气的,它不同意。

private 可以用来修饰类的构造方法、字段和方法,只能被当前类进行访问。protected 也可以用来修饰类的构造方法、字段和方法,但它的权限范围更宽一些,可以被同一个包中的类进行访问,或者当前类的子类。

可以通过下面这张图来对比一下四个权限修饰符之间的差别:

  • 同一个类中,不管是哪种权限修饰符,都可以访问;
  • 同一个包下,private 修饰的无法访问;
  • 子类可以访问 public 和 protected 修饰的;
  • public 修饰符面向世界,哈哈,可以被所有的地方访问到。

摘抄自:https://mp.weixin.qq.com/s/ySGMnBLhKtBnztivj_ImEg

关键字属于保留字,在 Java 中具有特殊的含义,比如说 public、final、static、new 等等,它们不能用来作为变量名。为了便于你作为参照,我列举了 48 个常用的关键字,你可以瞅一瞅。

abstract: abstract 关键字用于声明抽象类——可以有抽象和非抽象方法。

boolean: boolean 关键字用于将变量声明为布尔值类型,它只有 true 和 false 两个值。

break: break 关键字用于中断循环或 switch 语句。

byte: byte 关键字用于声明一个可以容纳 8 个比特的变量。

case: case 关键字用于在 switch 语句中标记条件的值。

catch: catch 关键字用于捕获 try 语句中的异常。

char: char 关键字用于声明一个可以容纳无符号 16 位比特的 Unicode 字符的变量。

class: class 关键字用于声明一个类。

continue: continue 关键字用于继续下一个循环。它可以在指定条件下跳过其余代码。

default: default 关键字用于指定 switch 语句中除去 case 条件之外的默认代码块。

do: do 关键字通常和 while 关键字配合使用,do 后紧跟循环体。

double: double 关键字用于声明一个可以容纳 64 位浮点数的变量。

else: else 关键字用于指示 if 语句中的备用分支。

enum: enum(枚举)关键字用于定义一组固定的常量。

extends: extends 关键字用于指示一个类是从另一个类或接口继承的。

final: final 关键字用于指示该变量是不可更改的。

finally: finally 关键字和 try-catch 配合使用,表示无论是否处理异常,总是执行 finally 块中的代码。

float: float 关键字用于声明一个可以容纳 32 位浮点数的变量。

for: for 关键字用于启动一个 for 循环,如果循环次数是固定的,建议使用 for 循环。

if: if 关键字用于指定条件,如果条件为真,则执行对应代码。

implements: implements 关键字用于实现接口。

import: import 关键字用于导入对应的类或者接口。

instanceof: instanceof 关键字用于判断对象是否属于某个类型(class)。

int: int 关键字用于声明一个可以容纳 32 位带符号的整数变量。

interface: interface 关键字用于声明接口——只能具有抽象方法。

long: long 关键字用于声明一个可以容纳 64 位整数的变量。

native: native 关键字用于指定一个方法是通过调用本机接口(非 Java)实现的。

new: new 关键字用于创建一个新的对象。

null: 如果一个变量是空的(什么引用也没有指向),就可以将它赋值为 null。

package: package 关键字用于声明类所在的包。

private: private 关键字是一个访问修饰符,表示方法或变量只对当前类可见。

protected: protected 关键字也是一个访问修饰符,表示方法或变量对同一包内的类和所有子类可见。

public: public 关键字是另外一个访问修饰符,除了可以声明方法和变量(所有类可见),还可以声明类。main() 方法必须声明为 public。

return: return 关键字用于在代码执行完成后返回(一个值)。

short: short 关键字用于声明一个可以容纳 16 位整数的变量。

static: static 关键字表示该变量或方法是静态变量或静态方法。

strictfp: strictfp 关键字并不常见,通常用于修饰一个方法,确保方法体内的浮点数运算在每个平台上执行的结果相同。

super: super 关键字可用于调用父类的方法或者变量。

switch: switch 关键字通常用于三个(以上)的条件判断。

synchronized: synchronized 关键字用于指定多线程代码中的同步方法、变量或者代码块。

this: this 关键字可用于在方法或构造函数中引用当前对象。

throw: throw 关键字主动抛出异常。

throws: throws 关键字用于声明异常。

transient: transient 关键字在序列化的使用用到,它修饰的字段不会被序列化。

try: try 关键字用于包裹要捕获异常的代码块。

void: void 关键字用于指定方法没有返回值。

volatile: volatile 关键字保证了不同线程对它修饰的变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

while: 如果循环次数不固定,建议使用 while 循环。

摘抄自:https://mp.weixin.qq.com/s/ySGMnBLhKtBnztivj_ImEg

数据类型 默认值 大小
boolean false 1比特
char ‘\u0000’ 2字节
byte 0 1字节
short 0 2字节
int 0 4字节
long 0L 8字节
float 0.0f 4字节
double 0.0 8字节

01、布尔

布尔(boolean)仅用于存储两个值:true 和 false,也就是真和假,通常用于条件的判断。代码示例:

boolean flag = true;

02、byte

byte 的取值范围在 -128 和 127 之间,包含 127。最小值为 -128,最大值为 127,默认值为 0。

在网络传输的过程中,为了节省空间,常用字节来作为数据的传输方式。代码示例:

1
2
byte a = 10;
byte b = -10;

03、short

short 的取值范围在 -32,768 和 32,767 之间,包含 32,767。最小值为 -32,768,最大值为 32,767,默认值为 0。代码示例:

1
2
short s = 10000;
short r = -5000;

04、int

int 的取值范围在 -2,147,483,648(-2 ^ 31)和 2,147,483,647(2 ^ 31 -1)(含)之间,默认值为 0。如果没有特殊需求,整形数据就用 int。代码示例:

1
2
int a = 100000;
int b = -200000;

05、long

long 的取值范围在 -9,223,372,036,854,775,808(-2^63) 和 9,223,372,036,854,775,807(2^63 -1)(含)之间,默认值为 0。如果 int 存储不下,就用 long,整形数据就用 int。代码示例:

1
2
long a = 100000L; 
long b = -200000L;

为了和 int 作区分,long 型变量在声明的时候,末尾要带上大写的“L”。不用小写的“l”,是因为小写的“l”容易和数字“1”混淆。

06、float

float 是单精度的浮点数,遵循 IEEE 754(二进制浮点数算术标准),取值范围是无限的,默认值为 0.0f。float 不适合用于精确的数值,比如说货币。代码示例:

1
float f1 = 234.5f;

为了和 double 作区分,float 型变量在声明的时候,末尾要带上小写的“f”。不需要使用大写的“F”,是因为小写的“f”很容易辨别。

07、double

double 是双精度的浮点数,遵循 IEEE 754(二进制浮点数算术标准),取值范围也是无限的,默认值为 0.0。double 同样不适合用于精确的数值,比如说货币。代码示例:

1
double d1 = 12.3

那精确的数值用什么表示呢?最好使用 BigDecimal,它可以表示一个任意大小且精度完全准确的浮点数。针对货币类型的数值,也可以先乘以 100 转成整形进行处理。

Tips:单精度是这样的格式,1 位符号,8 位指数,23 位小数,有效位数为 7 位。

双精度是这样的格式,1 位符号,11 位指数,52 为小数,有效位数为 16 位。


取值范围取决于指数位,计算精度取决于小数位(尾数)。小数位越多,则能表示的数越大,那么计算精度则越高。

一个数由若干位数字组成,其中影响测量精度的数字称作有效数字,也称有效数位。有效数字指科学计算中用以表示一个浮点数精度的那些数字。一般地,指一个用小数形式表示的浮点数中,从第一个非零的数字算起的所有数字。如 1.24 和 0.00124 的有效数字都有 3 位。

08、char

char 可以表示一个 16 位的 Unicode 字符,其值范围在 ‘\u0000’(0)和 ‘\uffff’(65,535)(包含)之间。代码示例:

1
char letterA = 'A'; // 用英文的单引号包裹住。

摘抄自:https://mp.weixin.qq.com/s/ySGMnBLhKtBnztivj_ImEg

每个程序都需要一个入口,对于 Java 程序来说,入口就是 main 方法。

public static void main(String[] args) {}
public、static、void 这 3 个关键字在前面的内容已经介绍过了,如果觉得回去找比较麻烦的话,这里再贴一下:

public 关键字是另外一个访问修饰符,除了可以声明方法和变量(所有类可见),还可以声明类。main() 方法必须声明为 public。

static 关键字表示该变量或方法是静态变量或静态方法,可以直接通过类访问,不需要实例化对象来访问。

void 关键字用于指定方法没有返回值。

另外,main 关键字为方法的名字,Java 虚拟机在执行程序时会寻找这个标识符;args 为 main() 方法的参数名,它的类型为一个 String 数组,也就是说,在使用 java 命令执行程序的时候,可以给 main() 方法传递字符串数组作为参数。

java HelloWorld 沉默王二 沉默王三
javac 命令用来编译程序,java 命令用来执行程序,HelloWorld 为这段程序的类名,沉默王二和沉默王三为字符串数组,中间通过空格隔开,然后就可以在 main() 方法中通过 args[0] 和 args[1] 获取传递的参数值了。

1
2
3
4
5
6
7
8
9
10
11
public class HelloWorld {
public static void main(String[] args) {
if ("沉默王二".equals(args[0])) {

}

if ("沉默王三".equals(args[1])) {

}
}
}

main() 方法的写法并不是唯一的,还有其他几种变体,尽管它们可能并不常见,可以简单来了解一下。

第二种,把方括号 [] 往 args 靠近而不是 String 靠近:

public static void main(String []args) { }
第三种,把方括号 [] 放在 args 的右侧:

public static void main(String args[]) { }
第四种,还可以把数组形式换成可变参数的形式:

public static void main(String...args) { }
第五种,在 main() 方法上添加另外一个修饰符 strictfp,用于强调在处理浮点数时的兼容性:

public strictfp static void main(String[] args) { }
也可以在 main() 方法上添加 final 关键字或者 synchronized 关键字。

第六种,还可以为 args 参数添加 final 关键字:

public static void main(final String[] args) { }
第七种,最复杂的一种,所有可以添加的关键字统统添加上:

final static synchronized strictfp void main(final String[] args) { }
当然了,并不需要为了装逼特意把 main() 方法写成上面提到的这些形式,使用 IDE 提供的默认形式就可以了。

摘抄自:https://mp.weixin.qq.com/s/ySGMnBLhKtBnztivj_ImEg

在 Java 中,我们使用 package(包)对相关的类、接口和子包进行分组。这样做的好处有:

使相关类型更容易查找
避免命名冲突,比如说 com.itwanger.Hellocom.itwangsan.Hello 不同
通过包和访问权限控制符来限定类的可见性

01、创建一个包

package com.itwanger;
可以使用 package 关键字来定义一个包名,需要注意的是,这行代码必须处于一个类中的第一行。强烈建议在包中声明类,不要缺省,否则就失去了包结构的带来的好处。

包的命名应该遵守以下规则:

  • 应该全部是小写字母
  • 可以包含多个单词,单词之间使用“.”连接,比如说 java.lang
  • 名称由公司名或者组织名确定,采用倒序的方式,比如说,我个人博客的域名是 www.itwanger.com,所以我创建的包名是就是 com.itwanger.xxxx
    每个包或者子包都在磁盘上有自己的目录结构,如果 Java 文件时在 com.itwanger.xxxx 包下,那么该文件所在的目录结构就应该是 com->itwanger->xxxx

02、使用包

让我们在名为 test 的子包里新建一个 Cmower 类:

1
2
3
4
5
6
package com.itwanger.test;

public class Cmower {
private String name;
private int age;
}

如果需要在另外一个包中使用 Cmower 类,就需要通过 import 关键字将其引入。有两种方式可供选择,第一种,使用 * 导入包下所有的类:

import com.itwanger.test.*;

第二种,使用类名导入该类:

import com.itwanger.test.Cmower;
Java 和第三方类库提供了很多包可供使用,可以通过上述的方式导入类库使用。

1
2
3
4
5
6
7
8
9
10
11
package com.itwanger.test;

import java.util.ArrayList;
import java.util.List;

public class CmowerTest {
public static void main(String[] args) {
List<Cmower> list = new ArrayList<>();
list.add(new Cmower());
}
}

03、全名

有时,我们可能会使用来自不同包下的两个具有相同名称的类。例如,我们可能同时使用 java.sql.Date 和 java.util.Date。当我们遇到命名冲突时,我们需要对至少一个类使用全名(包名+类名)。

1
2
List<com.itwanger.test.Cmower> list1 = new ArrayList<>();
list.add(new com.itwanger.test.Cmower());

摘抄自:https://mp.weixin.qq.com/s/ySGMnBLhKtBnztivj_ImEg