前言
功能分类
类(class)的第一个功能是隔离,起到边界的作用,使得不同功能的代码互不干扰。
干扰的起源
在非面向对象的语言中,我们主要定义结构和函数来实现功能。下边用C语言来举个例子。
某程序员写了宠物模拟器,不过开始他只有一只狗,于是他写了一个只有狗子特性的代码,代码功能就是模拟模拟狗子被撸后,会㕵㕵叫:
注:C语言代码使用Visual Studio编写。
#include<stdio.h>
#include<locale.h>
const wchar_t* name = L"旺才";
void call() {
wprintf(L"%s,过来!\n",name);
}
void pat() {
wprintf(L"汪汪\n");
}
void main() {
setlocale(LC_ALL, "");
wprintf(L"撸狗\n-----------\n");
call();
pat();
}
运行结果:
此时这个程序没啥问题,接着又养了一只猫,猫跟狗差不多的功能,但我们不能重复定义call()和pat()函数,这样会有冲突,那需要对代码做些改动,可以接受两只宠物。
这里至少有两种思路,一种是再复制狗的代码,改下名称;另一种,保持函数名称不变,添加参数进行区别。
调整1
对于前者,可能改成这样:
#include<stdio.h>
#include<locale.h>
const wchar_t* dog_name = L"旺才";
const wchar_t* cat_name = L"花花";
void dog_call() {
wprintf(L"%s,过来!\n", dog_name);
}
void cat_call() {
wprintf(L"%s,过来!\n", cat_name);
}
void dog_pat() {
wprintf(L"汪汪\n");
}
void cat_pat() {
wprintf(L"喵喵\n");
}
void main() {
setlocale(LC_ALL, "");
char c = '0';
while (c != 'q') {
if (c != '\n')
wprintf(L">>\n请选择宠物:狗(d),猫(c)\n");
c = getchar();
if (c == '\n') continue;
if (c == 'd') {
wprintf(L"撸狗\n-----------\n");
dog_call();
dog_pat();
}
else if (c == 'c') {
wprintf(L"撸猫\n-----------\n");
cat_call();
cat_pat();
}
else if (c != 'q') {
wprintf(L"所先宠物不存在\n");
}
}
}
运行结果:
调整2
另一种改成这样:
#include<stdio.h>
#include<locale.h>
const wchar_t* dog_name = L"旺才";
const wchar_t* cat_name = L"花花";
void call(char pet) {
if (pet == 'd')
wprintf(L"%s,过来!\n", dog_name);
else if (pet == 'c')
wprintf(L"%s,过来!\n", cat_name);
else
wprintf(L"所选宠物不存在\n");
}
void pat(char pet) {
if (pet == 'd')
wprintf(L"汪汪\n");
else if (pet == 'c')
wprintf(L"喵喵\n");
}
void main() {
setlocale(LC_ALL, "");
char c = '0';
while (c != 'q') {
if (c != '\n')
wprintf(L">>\n请选择宠物:狗(d),猫(c)\n");
c = getchar();
if (c == '\n') continue;
if (c == 'd') {
wprintf(L"撸狗\n-----------\n");
}
else if (c == 'c') {
wprintf(L"撸猫\n-----------\n");
}
call(c);
pat(c);
}
}
运行结果:
比较:
前者命名冲突,代码重复。好处是比较直观,小白都能看懂。
后者代码整洁,但交叉比较,耦合性高,牵一发可能动全身。
总结
不管用哪一种代码改动,多多少少都会对别的部分形成干扰,虽然有方法尽量去避免这些干扰,但往往对程序员的要求太高,反而降低了开发效率。产生干扰的主要原因是,C语言只能做到函数级别的代码分组,增加功能就要不断地增加函数,当函数的数据到达一定级别时,名称的冲突必然导致系统的不可维护。我们可以想像一下,当这个程序有了几百上千种宠物后,代码会是什么样子的?
用类(class)消除干扰
类实现了更大范围的代码分组,将相关的函数变成成员方法,将全局变量变成成员字段放在一个类中,与其它代码隔离开来,在小范围内可以有效地避开命名冲突的问题。就好比,行政区中全国乡镇的名称重复的很多,但在同一个区县中就不存在了。为了区分同名的乡镇,我们只要区分区县就可以了。所以前边的代码,用java的方式,我们就可以写成两个类,一个Dog类,一个Cat类,虽然他们有相同的部分,但互不干扰。
Dog类
/**
* 狗狗
*
*/
public class Dog {
private static String name = "旺财";
public static void call() {
System.out.format("%s\n", name);
}
public static void pat() {
System.out.println("汪汪!");
}
}
Cat类
/**
* 猫猫
*
*/
public class Cat {
private static String name = "花花";
public static void call() {
System.out.format("%s\n", name);
}
/**
* 猫会妙妙
*/
public static void pat() {
System.out.println("喵喵");
}
}
入口类
import java.io.IOException;
public class App {
public static void main(String[] args) throws IOException {
char c = '0';
while (c != 'q') {
if (c != '\n' && c != '\r')
System.out.println(">>\n请选择宠物:狗(d),猫(c)\n");
c = (char) System.in.read();
if (c == '\n' || c == '\r')
continue;
if (c == 'd') {
System.out.println("撸狗\n-----------\n");
Dog.call();
Dog.pat();
} else if (c == 'c') {
System.out.println("撸猫\n-----------\n");
Cat.call();
Cat.pat();
} else if (c != 'q') {
System.out.println("所先宠物不存在\n");
}
}
}
}
运行结果:
总结
JDK中的一些实现
Math (Java SE 17 & JDK 17)declaration: module: java.base, package: java.lang, class: Mathhttps://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Math.html System (Java SE 17 & JDK 17)declaration: module: java.base, package: java.lang, class: Systemhttps://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/System.html
Executors (Java SE 17 & JDK 17)declaration: module: java.base, package: java.util.concurrent, class: Executorshttps://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Executors.html
尝试
用Java重构之后,加宠物就方便很多了,只要类不重名,其它代码随便写。比如增加一个Parrot类:
/**
* 鹦鹉
*
*/
public class Parrot {
private static String name = "鹦哥";
public static void call() {
System.out.format("%s\n", name);
}
public static void pat() {
System.out.println("烦死了!");
}
}
修改App.java
import java.io.IOException;
public class App {
public static void main(String[] args) throws IOException {
char c = '0';
while (c != 'q') {
if (c != '\n' && c != '\r')
System.out.println(">>\n请选择宠物:狗(d),猫(c),鹦鹉(p)\n");
c = (char) System.in.read();
if (c == '\n' || c == '\r')
continue;
if (c == 'd') {
System.out.println("撸狗\n-----------\n");
Dog.call();
Dog.pat();
} else if (c == 'c') {
System.out.println("撸猫\n-----------\n");
Cat.call();
Cat.pat();
} else if (c == 'p') {
System.out.println("撸鹦鹉\n-----------\n");
Parrot.call();
Parrot.pat();
} else if (c != 'q') {
System.out.println("所先宠物不存在\n");
}
}
}
}
运行结果:
代码重用
前边,虽然解决了代码中的某些命名冲突的问题,但有个最大的缺点,就是重复率太高,如果不看具体实现,字段、方法的名称几乎是一样的。其实这些内容是可以做到重用的,Java的代码重用除了传统的定义数据结构和通用函数外,还可以类型化(Type)及继承。
类型化(Type)
前边的示例中,一个类只实现了一个宠物的行为,如果又增加了一只狗狗,是不是要增加一个类,比如Dog1,其实大可不必。前边之所以只能表述一个宠物,是因为类的成员都标成了static,意思就是这个类中的代码跟面向过程没啥差别,只是调用前需要加上类名作为前辍。增加的狗狗目前来说,可能只是名字不一样,所以只要一个狗狗类的模板即可。
Java的类除了分组的作用,本身也是可以作为一个数据类型来使用的,此时只要将类成员的static关键去掉即可。接下来改下Dog类、Cat类和Parrot类。
Dog类
/**
* 狗狗
*
*/
public class Dog {
private String name = "旺财";
public void call() {
System.out.format("%s\n", name);
}
public void pat() {
System.out.println("汪汪!");
}
}
Cat类
/**
* 猫猫
*
*/
public class Cat {
private String name = "花花";
public void call() {
System.out.format("%s\n", name);
}
/**
* 猫会妙妙
*/
public void pat() {
System.out.println("喵喵");
}
}
/**
* 鹦鹉
*
*/
public class Parrot {
private String name = "鹦哥";
public void call() {
System.out.format("%s\n", name);
}
public void pat() {
System.out.println("烦死了!");
}
}
此时,各个类就成类型(Type)结构了,如果调用这些代码,就得先创建类型实例,更改App类的代码,变成这样:
import java.io.IOException;
public class App {
public static void main(String[] args) throws IOException {
char c = '0';
while (c != 'q') {
if (c != '\n' && c != '\r')
System.out.println(">>\n请选择宠物:狗(d),猫(c),鹦鹉(p)\n");
c = (char) System.in.read();
if (c == '\n' || c == '\r')
continue;
if (c == 'd') {
System.out.println("撸狗\n-----------\n");
Dog dog = new Dog();
dog.call();
dog.pat();
} else if (c == 'c') {
System.out.println("撸猫\n-----------\n");
Cat cat = new Cat();
cat.call();
cat.pat();
} else if (c == 'p') {
System.out.println("撸鹦鹉\n-----------\n");
Parrot parrot = new Parrot();
parrot.call();
parrot.pat();
} else if (c != 'q') {
System.out.println("所先宠物不存在\n");
}
}
}
}
虽然多了点代码,但我们可以复用这个结构,只要做少量的修改,就可以创建无数个宠物。改一下Dog类,增加两个构造器,一个是默认的无参数,另一个是带参数的,这样可以创建对象的修改狗狗的名字:
/**
* 狗狗
*
*/
public class Dog {
private String name = "旺财";
public Dog() {
}
public Dog(String dogName) {
name = dogName;
}
public void call() {
System.out.format("%s\n", name);
}
public void pat() {
System.out.println("汪汪!");
}
}
App类
import java.io.IOException;
public class App {
public static void main(String[] args) throws IOException {
char c = '0';
while (c != 'q') {
if (c != '\n' && c != '\r')
System.out.println(">>\n请选择宠物:狗(d),猫(c),鹦鹉(p)\n");
c = (char) System.in.read();
if (c == '\n' || c == '\r')
continue;
if (c == 'd') {
System.out.println("撸两条狗\n-----------\n");
Dog dog = new Dog();
dog.call();
dog.pat();
Dog dog1 = new Dog("小贝");
dog1.call();
dog1.pat();
} else if (c == 'c') {
System.out.println("撸猫\n-----------\n");
Cat cat = new Cat();
cat.call();
cat.pat();
} else if (c == 'p') {
System.out.println("撸鹦鹉\n-----------\n");
Parrot parrot = new Parrot();
parrot.call();
parrot.pat();
} else if (c != 'q') {
System.out.println("所先宠物不存在\n");
}
}
}
}
运行效果:
继承
另一种代码重用的方式就是继承了,上述示例中无论狗狗、猫咪还是鹦鹉,它们行为方式是一样的,都是撸完后做出打出一行文本,无非就是内容上的差别,我们可以相同的部分提取出,单独创建一类在存放,响应内容的由具体的宠物类中实现,我们创建一个Pet类:
/**
* 小可爱们
*
*/
public class Pet {
private String name = "无名";// 宠物都有名字
public Pet() {
}
public Pet(String petName) {
name = petName;
}
public void call() {
System.out.format("%s\n", name);
}
public void pat() {
System.out.println(response());
}
/**
* 默认没反应,由具体对象实现
* @return
*/
protected String response() {
return "";
}
}
然后修改各个宠物类:
/**
* 狗狗
*
*/
public class Dog extends Pet {
public Dog() {
super();
}
public Dog(String dogName) {
super(dogName);
}
@Override
protected String response() {
return "汪汪!";
}
}
/**
* 猫猫
*
*/
public class Cat extends Pet {
public Cat() {
this("花花");
}
public Cat(String catName) {
super(catName);
}
@Override
protected String response() {
return "喵喵!";
}
}
/**
* 鹦鹉
*
*/
public class Parrot extends Pet {
public Parrot() {
this("鹦哥");
}
public Parrot(String parrotName) {
super(parrotName);
}
@Override
protected String response() {
return "烦死了!";
}
}
运行结果:
总结
为了实现代码的重用,实际还是有点代价的,代码的结构及语法都复杂了不少,但有了IDE的支撑,以后往后代码的增多,这点代价还是非常值的。接下来可以尝试增加两个宠物,一个是黄金莽,另一个是猫的细分种类咖啡猫。
对于黄金莽,即不需要名字,也不会发声,代码可以写成这样,基本上不用写什么代码:
/**
* 黄金莽
*
*/
public class GoldBoa extends Pet {
public GoldBoa() {
super("");
}
}
对于 咖啡猫,除了跟普通猫一样,还会说人话:
/**
* 咖啡猫
*
*/
public class Garfield extends Cat {
public Garfield() {
super("");
}
public Garfield(String name) {
super(name);
}
public void speak() {
System.out.print("我会说话!");
}
}
修改一下App,创建新的宠物:
import java.io.IOException;
public class App {
public static void main(String[] args) throws IOException {
char c = '0';
while (c != 'q') {
if (c != '\n' && c != '\r')
System.out.println(">>\n请选择宠物:狗(d),猫(c),鹦鹉(p),黄金莽(b)\n");
c = (char) System.in.read();
if (c == '\n' || c == '\r')
continue;
if (c == 'd') {
System.out.println("撸两条狗\n-----------\n");
Dog dog = new Dog();
dog.call();
dog.pat();
Dog dog1 = new Dog("小贝");
dog1.call();
dog1.pat();
} else if (c == 'c') {
System.out.println("撸猫\n-----------\n");
Cat cat = new Cat();
cat.call();
cat.pat();
Garfield garfield = new Garfield();
garfield.call();
garfield.pat();
garfield.speak();
} else if (c == 'p') {
System.out.println("撸鹦鹉\n-----------\n");
Parrot parrot = new Parrot();
parrot.call();
parrot.pat();
} else if (c == 'b') {
System.out.println("撸黄金莽\n-----------\n");
GoldBoa boa = new GoldBoa();
boa.call();
boa.pat();
} else if (c != 'q') {
System.out.println("所先宠物不存在\n");
}
}
}
}
运行结果:
访问控制
代码进行分组后,还可以对内部成员进行访问控制。这样可以对代码进行更多的约束控制,上述示例中,每个宠物都有自己的名字,名字一旦起好,别人就不能随意发动了,所以在定义类的时候,name这个字段前边加上了private这个修饰符,说明不能被外部更改,因为加了这个修饰后,程序运行起来后,这个代码对外部的代码是不可见的。就好比路上见到一个人,你不知道这个有没有带钱包,因为钱包是私有的,一般是不让外人看到的。
私有(private)与公开(public)
宠物的名字虽然不可修改,但我们还是可以得到宠物的名字的,然后修改一下Pet类,增加一个获取名字的方法,这个方法是公开的,也就说谁都可以知道宠物的名字,所以前边加了一个修改符public:
/**
* 小可爱们
*
*/
public class Pet {
private String name = "无名";// 宠物都有名字
public Pet() {
}
public Pet(String petName) {
name = petName;
}
public String getName() {
return name;
}
public void call() {
System.out.format("%s\n", name);
}
public void pat() {
System.out.println(response());
}
/**
* 默认没反应,由具体对象实现
*
* @return
*/
protected String response() {
return "";
}
}
再改下App类:
import java.io.IOException;
public class App {
public static void main(String[] args) throws IOException {
char c = '0';
while (c != 'q') {
if (c != '\n' && c != '\r')
System.out.println(">>\n请选择宠物:狗(d),猫(c),鹦鹉(p),黄金莽(b)\n");
c = (char) System.in.read();
if (c == '\n' || c == '\r')
continue;
if (c == 'd') {
System.out.println("撸两条狗\n-----------\n");
Dog dog = new Dog();
System.out.format("宠物名字:%s\n",dog.getName());
dog.call();
dog.pat();
Dog dog1 = new Dog("小贝");
System.out.format("宠物名字:%s\n",dog1.getName());
dog1.call();
dog1.pat();
} else if (c == 'c') {
System.out.println("撸猫\n-----------\n");
Cat cat = new Cat();
System.out.format("宠物名字:%s\n",cat.getName());
cat.call();
cat.pat();
Garfield garfield = new Garfield();
System.out.format("宠物名字:%s\n",garfield.getName());
garfield.call();
garfield.pat();
garfield.speak();
} else if (c == 'p') {
System.out.println("撸鹦鹉\n-----------\n");
Parrot parrot = new Parrot();
System.out.format("宠物名字:%s\n",parrot.getName());
parrot.call();
parrot.pat();
} else if (c == 'b') {
System.out.println("撸黄金莽\n-----------\n");
GoldBoa boa = new GoldBoa();
System.out.format("宠物名字:%s\n",boa.getName());
boa.call();
boa.pat();
} else if (c != 'q') {
System.out.println("所先宠物不存在\n");
}
}
}
}
运行效果:
保护(protected)
protected这个关键字是继承时专用的。表示可以提供给子类访问,这样的话就可以将一些方法让子类调用、重写或实现。之前代码中已经在Pet类中定义了一个叫做response()的方法,这个方法在App中是不可见的,但Dog、Cat等类中是可以访问的。具体请查看之前的代码,这里不复述。
局部实现
内部类
之前,我们撸宠物的时候,宠物是直接做出反应的,但有时候可能是反应迟钝的,这时可以用线程来模拟一下,线程需要写一个类实现Runnable接口,但这种反应是每个动物特有的,所以不必定义全局类,只定义一个内部类来实现,以狗子举例:
/**
* 狗狗
*
*/
public class Dog extends Pet {
public Dog() {
this("汪财");
}
public Dog(String dogName) {
super(dogName);
}
@Override
protected String response() {
return "汪汪!";
}
@Override
public void pat() {
Thread daze = new Thread(new DogResponse());
daze.start();
}
class DogResponse implements Runnable {
@Override
public void run() {
try {
System.out.println("先发个呆!");
Thread.sleep(3000);
System.out.println(response());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
为了方便起见,再改下App, 去掉一只狗狗:
import java.io.IOException;
public class App {
public static void main(String[] args) throws IOException {
char c = '0';
while (c != 'q') {
if (c != '\n' && c != '\r')
System.out.println(">>\n请选择宠物:狗(d),猫(c),鹦鹉(p),黄金莽(b)\n");
c = (char) System.in.read();
if (c == '\n' || c == '\r')
continue;
if (c == 'd') {
System.out.println("撸两条狗\n-----------\n");
Dog dog = new Dog();
System.out.format("宠物名字:%s\n",dog.getName());
dog.call();
dog.pat();
} else if (c == 'c') {
System.out.println("撸猫\n-----------\n");
Cat cat = new Cat();
System.out.format("宠物名字:%s\n",cat.getName());
cat.call();
cat.pat();
Garfield garfield = new Garfield();
System.out.format("宠物名字:%s\n",garfield.getName());
garfield.call();
garfield.pat();
garfield.speak();
} else if (c == 'p') {
System.out.println("撸鹦鹉\n-----------\n");
Parrot parrot = new Parrot();
System.out.format("宠物名字:%s\n",parrot.getName());
parrot.call();
parrot.pat();
} else if (c == 'b') {
System.out.println("撸黄金莽\n-----------\n");
GoldBoa boa = new GoldBoa();
System.out.format("宠物名字:%s\n",boa.getName());
boa.call();
boa.pat();
} else if (c != 'q') {
System.out.println("所先宠物不存在\n");
}
}
}
}
运行效果:
匿名类
如果嫌定义内部类麻烦的话,也可以使用匿名类,使代码更紧凑一点,狗狗的代码变成了这样:
/**
* 狗狗
*
*/
public class Dog extends Pet {
public Dog() {
this("汪财");
}
public Dog(String dogName) {
super(dogName);
}
@Override
protected String response() {
return "汪汪!";
}
@Override
public void pat() {
Thread daze = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("先发个呆!");
Thread.sleep(3000);
System.out.println("3秒后......");
System.out.println(response());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
daze.start();
}
}
运行效果同上。
局部类
宠物养了就是玩的,有时候会让宠物表演,当然不是真的表演,而随机做一些动物,我们可以定义一个类来存放宠物表演时的状态,但表演也是经常性的,而且仅仅是表演时用到,所以不必定义一个全局类来表示这些动作,也不用定义内部类,别的地方用不到,因而可以直接在方法内定义一个局部类,以狗狗为例:
/**
* 狗狗
*
*/
public class Dog extends Pet {
public Dog() {
this("汪财");
}
public Dog(String dogName) {
super(dogName);
}
@Override
protected String response() {
return "汪汪!";
}
public void performence() {
class Actions {
public int times = 0;
public void bark() {
System.out.println("汪汪!汪汪汪!");
}
public void sit() {
System.out.println("坐下了!");
}
public void jump() {
System.out.println("上屋顶了!");
}
}
System.out.println("----------开始表演--------------");
Actions actions = new Actions();
while (actions.times < 5) {
int order = (int) Math.ceil(Math.random() * 3);
switch (order) {
case 1:
actions.bark();
break;
case 2:
actions.sit();
case 3:
actions.jump();
}
actions.times++;
}
System.out.println("----------表演结束--------------");
}
@Override
public void pat() {
Thread daze = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("先发个呆!");
Thread.sleep(3000);
System.out.println("3秒后......");
System.out.println(response());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
daze.start();
}
}
运行效果:
总结
类是用来分组代码功能的,有时候会有一些使用范围比较小的代码,而又要对其按功能分组,就可以使用上述的实现方法。
一些细节
静态成员
本文刚开始的时候,定义的类成员都是加了static的,static表示程序一运行,这些代码就可以直接使用的,仅仅加上类名作前缀即可。如果不加就要先做new的这个动作,才能使用。也就是static与非static是两个不同的东西。因为先有static,所以在new出来的对象实例中使用static成员,反之则不能,很可能实例还没创建,没东西可用,即使他们在一个类里边。
静态类
顶层类不能加static修饰,但内部类却可以。
Lamda
匿名类有些情况下可以使用Lamda来表达,这样可以使代码更加紧凑。比如前边狗狗被的反应可以变成这样(Thead类的构造方法参数):
/**
* 狗狗
*
*/
public class Dog extends Pet {
public Dog() {
this("汪财");
}
public Dog(String dogName) {
super(dogName);
}
@Override
protected String response() {
return "汪汪!";
}
public void performence() {
class Actions {
public int times = 0;
public void bark() {
System.out.println("汪汪!汪汪汪!");
}
public void sit() {
System.out.println("坐下了!");
}
public void jump() {
System.out.println("上屋顶了!");
}
}
System.out.println("----------开始表演--------------");
Actions actions = new Actions();
while (actions.times < 5) {
int order = (int) Math.ceil(Math.random() * 3);
switch (order) {
case 1:
actions.bark();
break;
case 2:
actions.sit();
case 3:
actions.jump();
}
actions.times++;
}
System.out.println("----------表演结束--------------");
}
@Override
public void pat() {
Thread daze = new Thread(() -> {
try {
System.out.println("先发个呆!");
Thread.sleep(3000);
System.out.println("3秒后......");
System.out.println(response());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
daze.start();
}
}
用Lamda只要写run()中的代码就可以了。
总结
概念这东西,很多时候是用的一些新造的词来描述的,Java是一门面向对象的语言,面向对象这东西从字面上并不是很好理解。封装是什么?继承是什么?多态是什么?并不太好用自然语言来表达,多数解释也只是说明了其意图,并不能解释是如何实现的。其实代码写多了,也就那么回事。