Java进阶-类和对象

开始

前面已经多次提到面向对象是Java语言的精髓,没有学过面向对象的人是写不出优秀的Java。那么开始这一节让各位期待良久(才怪嘞)了的课吧~

**↑不来一首BGM吗?**

本章要求

  • 定义类
  • 类的属性
  • 类的行为(又称为方法)
  • 创建对象
  • 权限关键字

定义类

先来看代码:

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
//定义一个名称为Book的类
class Book {
//为Book类定义一个String类型的属性
private String name;
//为Book类定义一个int类型的属性
private int ID;
//构造方法
public Book(String name, int ID) {
//使用this关键词将获取到全局变量,屏蔽局部变量
this.name = name;
this.ID = ID;
}
//定义行为:获取书的名字
public String getName() {
return name;
}
//定义行为:获取书的ID
public int getID() {
return ID;
}
//定义行为:让书爆炸吧!
//↑什么鬼!
public void boom() {
System.out.println(this.name + "爆炸了一次!");
}
}

虽然有注释,但是我猜到你们这群(sb)还是会看不懂,没办法呢,那么我来解释这些代码:
Book类,也就是书类,在我的代码里,一本书是有ID和名称的,因此我为他赋上属性:ID 和 name
这个ID和name也不是空在那里没有用的,我需要在其他代码里获取这个name和ID,于是我设置行为:getName和getID来分别返回这本书的Name和ID。
而且这个行为是有返回值的(除非你写个void返回值),就比如说我们那个getName行为,返回值就是一个String。
如何定义一个行为?如下格式:
权限修饰符 返回值 行为名称(行为要接收的数据) {代码块...}
权限修饰符待会再讲…

还有差点被我忘掉的构造方法:这个方法在类被实例化时调用,如果你不写这个方法,系统会自动为你创建一个什么都不做的构造方法。它是一个没有返回值,而且固定为public的方法。我这个Book被创建的时候需要初始化它的ID和name。因此,我使用构造方法,接收一个String和int值并保存下来。
总之,做完这些,你的Java就知道了“噢,Book就是这么个东西”
(不仅如此他还知道“你个sb写的什么弱鸡代码还好意思拿给我看你都不嫌丢脸的吗快滚吧你”)
在构造方法中,我们让外部传递了一个String和int过来,这两个值将在这个方法中成为名为name和ID的局部变量。你知道,局部变量再越过花括号}就将死亡,而我们的name和ID是以后还需要用到的,因此我们需要把它存为全局变量,全局变量可以一直存在,直到这个对象死亡。而我们下面的getID和getName方法return的都是全局变量name和ID

创建对象

类不是对象,它仅仅是创建对象的一个模板,仅仅声明一个类是不会有任何效果的,我们需要在main函数的代码里搞♂出来一个对象,然后对它做一些事♂情。
看代码:

1
2
3
4
5
6
public static void main(String[] args) {
//以Book类为模板创建对象
Book my_book = new Book("金瓶梅",0);
//使用Book类的一些行为
System.out.println(my_book.getName() + "的ID是: " + my_book.getID());
}

这里要讲一下的就是new这个关键词了,它是用来实例化对象的关键词。
之前将数组的时候有提到这个,数组也是对象,所以需要用new来创建数组。

一个普通类在没有实例化之前,就是new之前,它的属性,方法等等在内存中都是不存在的.只有new了以后,这个类的一些东西在内存中才会真的存在,也就是说只有new了之后,这个类才能用.
出自百度知道

关于括号里的"金瓶梅",0,你应该猜得到:“金瓶梅”将成为my_book这个对象的name,0将成为它的ID。
而后的代码里我使用getName和getID来再次获取它。

权限关键字

属性和行为都是有权限的,你可以设置public属性,这样所有代码都能直接通过对象名.属性名来访问这个属性(但是我不推荐这么做)
行为也是有权限的,private行为将只有本类自己能访问,外部无法访问。
我先列一下这个权限表

||public|protected|default|private|
|——|——|——||——||——||——|
|本类|√|√|√|√|
|同包的其他类|√|√|√|×|
|子类\父类|√|√|×|×|
|其他包的其他类|√|×|×|×|

public:公共的
protected:受保护的
default:默认的
private:私有的


这么一张表应该很清晰了,举个例子:
如果我Book的name是public权限,那么我main函数里就可以直接my_book.name来获取到这本书的名字了。而且我还可以my_book.name = "Java从入门到入土"来更改这本书的名字。好吧,这样看起来,把属性设置为public权限是很方便的,那么为什么仓鼠说不推荐这么做呢?
因为这样,别人就能轻易获取并修改你对象的属性(噢!天呐!他居然真的敢把我女朋友给扎破!
再者,如果你某天把这个变量给改了一下名字…那么岂不是其他所有程序里有关到这个变量的代码都得重写?想一想:如果bukkit里的某个变量是这么做的,而某一天作者突然看到“乌鸦坐飞机”这个变量名很不爽,而把他改成了“龙卷风摧毁停车场”,那么所有使用了“乌鸦坐飞机”这个变量的插件都无法使用了!(当然这种情况基本上是不会发生的
什么?你说你不会智障到随便去改一个变量的名字?emmmm,那我再给你讲一个例子:
你写了一个游戏,还很聪明得为开发者们留了API,好让更多的人来为你的游戏想写插件增加新玩法。但是呢,因为你的代码里类的属性全都是public,导致了一些奇怪的问题:某个开发者写的代码逻辑出了一些问题,它可能将玩家的血量设置超过了100,然而玩家的最大血量是100,这就尴尬了:一个玩家的血量竟然超过了他的血量上限,这个bug导致了一些奇奇怪怪的问题。然而这位奇怪的开发者却并不觉得这是自己的错,他反而把锅推给你说:“你的程序容错性也太差了吧!为什么仅仅是血量设置超过上限就导致我的电脑爆炸了!”

于是你将你的游戏重写了,类的属性不再是public,而是被改成了private……嗯?那么我们要如何获取这些属性?
当然是getter方法啦:getName()、getID()……
如何设置这些属性呢?
自然是setter方法啦:setName(String name)、setID(int ID)、setHealth(int health)…
虽然这还不如直接p.health = 100来的简洁明了,但是它却保证了代码的安全性
别急,我们来看看他的setHealth的代码

1
2
3
4
5
6
7
8
public void setHealth(int health) {
if(health > 100) {
this.health = 100;
}
else {
this.health = health;
}
}

WoW!多么聪明的代码!这样玩家的血量无论如何都不会超过它的上限了~


看完上面的例子,你应该知道了我为啥不推荐直接将属性设为public:这样是极不安全的。使用setter来设置属性内容让我们的代码更安全。如果我们不希望外部代码更改我们类的属性,可以只设置getter而不留setter,反之亦然。

如果某个行为你不希望外部调用到,可以将它设置为private,比如说一段代码在你这个类里可能多次出现,那么你可以把这段代码单独包装起来,设置为private的行为,然后调用this.xxx();来使用这段重复的代码,这样可以完美的避免代码冗余的尴尬。

类也可以修饰为public,但是public类只能在他自己的文件中声明,比如如果Book类被声明为public,那么我只能在Book.java这个文件中写这段代码


另外:
包是用来方便管理代码的工具,比如说:人类和狗类都属于生物类,我们就可以把它放在一个专门存生物类代码的包中、书类和笔类都属于物品类,我们可以把他放在一个专门存物品类代码的包中。
怎么创建包?右键src –> 新建 –> 包 –> 输入包名
先别急着输入,我们来讲一讲Java包的命名规则
百度知道
其实要讲也没这么麻烦,总之注意两点:
1.不要有大写英文字母
2.包首以自己的域名或邮箱的翻转输入
例如我的包一般为:cn.viosin.项目名.xxx
我存这个生物类包就是cn.viosin.teachingprocedure.biology
我存这个物品类的包就是cn.viosin.teachingprocedure.item
讲了这么多,其实没乱用,你若非要用你那杀马特的包名也没人说你,反正别人翻你代码又不看你包名的(你那辣鸡代码你确定有人看?)

“说了这么多你还是没告诉我为啥那个书有‘爆炸’这个行为啊”
那只是用来凑字数的2333


静态变量、方法

在讲这个之前,我们得先来认识一下static关键字。也许你见过这个关键字,嗯….就是那个public static void main(String args[]) {}
之前有说过,你的程序启动的时候外部会调用这个方法,public是什么已经讲过了,void你也应该知道了它代表着无返回值的意思
为啥这个方法一定要叫main我也解释过,因为这是规定…
static是静态修饰符,它的作用是让一个变量或者方法可以超脱世俗。
一个public static的[属性/方法]能够直接被外部以 类名.xxx 调用。不仅如此,一个静态的属性,是所有该类的对象都共享的,它将成为这个类的对象的公共属性。
(区别于非公共属性,比如说狗,两只狗可以拥有不同的生命值,所以我们定义生命值属性时不需要使用staic)
他的生命周期不同于全局变量(对象死亡时消失)和局部变量(方法执行完则消失),静态属性只会在你的程序执行结束时才会死亡。
比如说你的Book类有一个静态属性public static int size;那么这个int size将被所有Book共享,不管你是Book book1 = new Book();还是Book book2 = new Book();或者是Book book3 = new Book();总之他们的size都是相同的,你改变了book1的size,那么book2和book3的size也会被改变。而且,就算你没有new出来book,你也可以直接Book.size = 2;这样子来更改它,除非你没有这个属性的访问权(比如它被设置为private了)

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
public class MyFirstClass {
public static void main(String[] args) {
Book book1 = new Book("金瓶梅", 0);
Book book2 = new Book("Java从入门到入土", 1);
Book book3 = new Book("如何攻略小羽酱", 2);
System.out.println(book1.getName() + " 的size是 " + book1.size);
book2.size = 20;
System.out.println(book1.getName() + " 的size变成了 " + book1.size);
Book.size = 30;
System.out.println(book3.getName() + " 的size又变成了 " + book3.size);
}
}
//定义一个名称为Book的类
class Book {
//为Book类定义一个String类型的属性
private String name;
//为Book类定义一个int类型的属性
private int ID;
//公共静态变量szie
public static int size;
//构造方法
public Book(String name, int ID) {
//使用this关键词将获取到全局变量,屏蔽局部变量
this.name = name;
this.ID = ID;
size = 10;
}
//定义行为:获取书的名字
public String getName() {
return name;
}
//定义行为:获取书的ID
public int getID() {
return ID;
}
//定义行为:让书爆炸吧!
//↑什么鬼!
public void boom() {
System.out.println(this.name + "爆炸了一次!");
}
}

输出为:

1
2
3
金瓶梅 的size是 10
金瓶梅 的size变成了 20
如何攻略小羽酱 的size又变成了 30

那么接下来我们再讲一讲静态方法:
用了这么久的main你也应该有了解一点吧,静态方法可以直接通过 类名.方法名(xxx)来使用
比如说,如果我让Book类有一个静态方法…

1
2
3
4
5
6
7
8
9
10
public class MyFirstClass {
public static void main(String[] args) {
Book.boom();
}
}
class Book {
public static void boom() {
System.out.println("Book毫无意义的boom了~");
}
}

值得一提的是,在静态方法里我们无法使用非静态的成员变量。(想一想,为什么?

对象的引用

在上一节中我们有说过,你得告诉Java你说的对象是哪个对象,代码中Book book1 = new Book("金瓶梅", 0);这样子在一个book新出生时我们就用book1指向他,以后我们再说book1的时候,Java就知道我们指的是这本书。
同理:Book book2 = new Book("如何攻略小羽酱", 1);就是用book2指向另一本刚出生的book类对象。
这样,Java就知道,book1是指金瓶梅这本书,book2是指如何攻略小羽酱这本书。你现在可以用book1和book2来做一些事。
嗯……但我们可不是来谈论如何让book1与book2交配的,我们来做一些残忍的事情:
我们book2 = boo1;,这样会发生什么?
你这样做了之后,book2变成指向book1的那本书了,也就是说,book2指向了金瓶梅,而book1也还是只想金瓶梅
那如何攻略小羽酱这本书呢?现在没有任何一个指针指向它,它将怎么样?
他要死了!
没错,它已经死了,你在世界上再也找不到完全和它一样的对象了。
(即使你再使用book2 = new Book("如何攻略小羽酱", 1);,但是由于在内存中的分配地址不一样,它和之前的那本如何攻略小羽酱还是不一样的233333)
这可真残忍呢,不是吗?
除此之外,你可以这样Book book1;来创建一个名称为book1的指针。尽管目前它并不指向任何Book,但是它就是存在,毫无意义的存在着。
如果你这个时候要求book1.getName();会怎么样呢?
Java会尝试找到book1指向的这本书,然后他发现,无论他如何努力都找不到这本书,最后只好死给你看了(甩你一脸空指针异常):

1
2
Exception in thread "main" java.lang.NullPointerException
at MyFirstClass.main(MyFirstClass.java:6)

关于异常的问题,我们以后会讲到的…

再谈this

前面说过,this.xxx可以获取到本类的全局变量,但是this并不仅仅只有这个作用
this是指向当前对象的指针,也就是指向自己的指针。
比如说,我们写一个狗打架的程序(毫无意义)
这个狗呢,可以attack(攻击),也可以hurt(受伤)
attack需要传递另一只狗过来好让这只狗知道他该打谁(先判断自己血量是不是低于0,不是再攻击)
而hurt呢也需要传递一只狗过来好让这只受伤的狗知道是谁打了他(先减少自己的血量,然后再调用自己的attack反击)
那么我们只需要在attack中调用另一只狗的hurt方法,然后把自己传递过去
那么我们就可以这样dog2.hurt(this);
没错,this指向自己,然后把自己传了过去23333

练习

算了我懒得布置练习了,来看看下面这一个毫无卵用的程序吧~

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import java.util.Random;
public class MyFirstClass {
public static void main(String[] args) {
//获取随机数
Random random = new Random();
int id = random.nextInt();
Dog dog1 = new Dog("狗群员", id++, random.nextInt(32768), random.nextInt(1000));
Dog dog2 = new Dog("狗群主", id++, random.nextInt(32768), random.nextInt(1000));
dog1.attack(dog2);
}
}
class Dog {
//名称
private String name;
//ID
private int id;
//血量
private int health;
//伤害
private int damage;
//行为:攻击一只狗
public Dog(String name, int id, int health, int damage) {
this.name = name;
this.id = id;
if(health <= 0) {
health = -health + 1000;
}
if(damage <= 0 ) {
damage = -damage + 1000;
}
this.health = health;
this.damage = damage;
System.out.println(name + "已上线, ID: " + id + " 血量: " + health + " 伤害: " + damage);
}
//行为: 攻击
//dog: 攻击的目标
public void attack(Dog dog) {
if(dog.getId() == this.id) {
System.out.println(this.name + " 并不想攻击自己");
}
else if(health <= 0) {
System.out.println(this.name + "已经狗带了,无法再攻击!");
}
else {
//让attack的目标受伤
dog.hurt(this, damage);
}
}
//行为:被一只狗攻击
//dog: 攻击来源
//damage: 伤害值
public void hurt(Dog dog, int damage) {
//减血
this.health = this.health - damage;
System.out.println(this.name + " 被 " + dog.getName() + " 攻击了,当前血量: " + this.health);
//反击
System.out.println(this.name + " 想要反击 " + dog.getName());
attack(dog);
}
public String getName() {
return name;
}
public int getId() {
return id;
}
}

这样当一只狗攻击另一只狗的时候,另一只狗就会反击,反过来攻击这只狗,而这只狗又反击…
无限循环直到其中一只go die了
执行后是这样的:

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
狗群员已上线, ID: 1775386723 血量: 6975 伤害: 960
狗群主已上线, ID: 1775386724 血量: 14832 伤害: 698
狗群主 被 狗群员 攻击了,当前血量: 13872
狗群主 想要反击 狗群员
狗群员 被 狗群主 攻击了,当前血量: 6277
狗群员 想要反击 狗群主
狗群主 被 狗群员 攻击了,当前血量: 12912
狗群主 想要反击 狗群员
狗群员 被 狗群主 攻击了,当前血量: 5579
狗群员 想要反击 狗群主
狗群主 被 狗群员 攻击了,当前血量: 11952
狗群主 想要反击 狗群员
狗群员 被 狗群主 攻击了,当前血量: 4881
狗群员 想要反击 狗群主
狗群主 被 狗群员 攻击了,当前血量: 10992
狗群主 想要反击 狗群员
狗群员 被 狗群主 攻击了,当前血量: 4183
狗群员 想要反击 狗群主
狗群主 被 狗群员 攻击了,当前血量: 10032
狗群主 想要反击 狗群员
狗群员 被 狗群主 攻击了,当前血量: 3485
狗群员 想要反击 狗群主
狗群主 被 狗群员 攻击了,当前血量: 9072
狗群主 想要反击 狗群员
狗群员 被 狗群主 攻击了,当前血量: 2787
狗群员 想要反击 狗群主
狗群主 被 狗群员 攻击了,当前血量: 8112
狗群主 想要反击 狗群员
狗群员 被 狗群主 攻击了,当前血量: 2089
狗群员 想要反击 狗群主
狗群主 被 狗群员 攻击了,当前血量: 7152
狗群主 想要反击 狗群员
狗群员 被 狗群主 攻击了,当前血量: 1391
狗群员 想要反击 狗群主
狗群主 被 狗群员 攻击了,当前血量: 6192
狗群主 想要反击 狗群员
狗群员 被 狗群主 攻击了,当前血量: 693
狗群员 想要反击 狗群主
狗群主 被 狗群员 攻击了,当前血量: 5232
狗群主 想要反击 狗群员
狗群员 被 狗群主 攻击了,当前血量: -5
狗群员 想要反击 狗群主
狗群员已经狗带了,无法再攻击!

本章完

点我返回目录

感谢各位的阅读!

人生不易,仓鼠断气