【Java转Go】如何理解面向对象,怎么把Golang用成面向对象的样子
面向对象就是封装、继承、多态。
# Java面向对象回顾
封装:
也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。封装是面向对象的特征之一,是对象和类概念的主要特性。
简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
在Java中Get/Set方法保护成员变量就是封装的体现,将成员变量用private修饰,使外部无法直接操作,仅通过public修饰的方法向外暴露,使其仅能通过对象计划内的方式修改。 例如
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
2
3
4
5
6
7
8
9
10
11
继承:
是指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现。继承概念的实现方式有二类:实现继承与接口继承。实现继承是指直接使用基类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力;
在Java中可以使用 子类extends父类 实现继承,子类将继承父类的成员变量和成员方法,子类可以添加自己特有的成员,体现出与父类的不同。
public class Student extends Person {
private Integer studentId;
public void sayHello() {
System.out.println("Hi,我叫" + this.getName() + "我的ID是" + studentId);
}
}
2
3
4
5
6
7
8
多态:
就是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
在Java中,将某个父类的子类对象,或接口的实现类对象,赋值给父类或接口引用,就是多态。
public interface SayHelloAble {
public void sayHello();
}
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Student extends Person implements SayHelloAble {
private Integer studentId;
public void sayHello() {
System.out.println("Hi,我叫" + this.getName() + "我的ID是" + this.studentId);
}
}
public class Teacher extends Person implements SayHelloAble {
private String major;
public void sayHello() {
System.out.println("Hi,我叫" + this.getName() + "我的专业是" + this.major);
}
}
public class Main {
public static void main(String[] args) {
Person person;
person = new Student();
person = new Teacher();
person.getName();
SayHelloAble sayHelloAble;
sayHelloAble = new Student();
sayHelloAble = new Teacher();
sayHelloAble.sayHello();
}
}
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
可以看到上边的例子里有一个关键字this,他指向调用方法的当前对象,该关键字在编辑时可以省略,但编译后是存在的。
我们稍微改写一下上面的例子,将对象方法修改为静态方法。
改写前:
...
public void sayHello() {
System.out.println("Hi,我叫" + this.getName() + "我的ID是" + this.studentId);
}
...
...
Student student = new Student();
student.sayHello();
...
2
3
4
5
6
7
8
9
改修后:
public static void sayHello(Student self){
System.out.println("Hi,我叫" + self.getName() + "我的ID是" + self.studentId);
}
...
Student student = new Student();
sayHello(student);
...
2
3
4
5
6
7
# Golang面向对象
好了,现在你应该能够理解,所谓的面向对象函数调用,就是用对象.对象函数,并将对象引用作为this传进去。
在Java中必须写一个类,实例化类后才能使用对象函数。而在Golang中没有类的概念,函数可以写在任何文件里。
在定义函数时可以定义一个接收器(receiver),他接受一个结构体(struct)或结构体指针,结构体是一个复合类型的值类型。接收器可以理解为一种语法糖,相当于前置了一个参数,并通过参数.函数的方式传递进来。
要注意,当传递的对象为结构体时,传递的是结构体值拷贝,对其修改无法改变函数外的值(结构体是浅拷贝,引用类型拷贝引用,会被外部修改,注意闭坑)。当传递结构体指针时,传递的是指针拷贝,和外部指针指向同一结构体,对其修改可以影响外部值。
具体可以看【Java转Go】如何理解Go中的值类型、引用类型、nil
我们尝试用Go写一个封装:
type Person struct {
name string
}
func (receiver *Person) getName() string {
return receiver.name
}
func (receiver *Person) setName(name string) {
receiver.name = name
}
2
3
4
5
6
7
8
9
Golang有内置的new函数,接受参数是一个结构体类型,为该类型的结构体开辟一块内存空间,填充成员默认值,返回其指针。引用类型的默认值为nil。 但更长用的是用结构体接{}直接声明一个结构体实例。如果需要取其指针,只要在前面加&。使用大括号的好处在于可以快速对成员做填充。
注: 如果你总是在声明结构体时做同样的动作,那你应该抽取一个返回该结构体实例的函数,这就相当于Java的构造函数。
func main() {
//Person
p1 := Person{}
p1.setName("张三")
//*Person
p2 := &Person{name: "李四"}
//*Person
p3 := new(Person)
p1.setName("王五")
}
2
3
4
5
6
7
8
9
10
接下来讲继承。
在Golang中结构体可以匿名嵌套,嵌套后可以直接访问被嵌套的结构体中的字段,也可以访问嵌套的结构体本身。
*你不总是需要用面相对象的方式去写Golang,Golang的设计遵循奥卡姆剃刀法则,如非必要请勿面向对象。*所以下面的演示将会直接暴露字段,而不是用Get/Set方法保护。Golang没有访问修饰符,首字母大写意味着公开,首字母小写意味着私有,私有的范围是当前包内可访问。
type Person struct {
name string
}
func (receiver *Person) sayHello() {
println("我是Person" + receiver.name)
}
type Student struct {
*Person
studentId int
}
func (receiver *Student) sayId() {
println("我的Student,我的ID是", receiver.studentId)
}
func main() {
student := Student{Person: &Person{name: "张三"}, studentId: 20}
//张三
println(student.name)
//20
println(student.studentId)
//我是Person张三
student.sayHello()
//我的ID是 20
student.sayId()
}
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
可以看到,通过匿名嵌套,Student结构体也可以直接通过.访问到嵌套进来的匿名结构体Person中的成员,或直接调用接收器类型为* Person的函数。
tips:当函数签名一致时,会有限按照当前结构体类型调用,相当于Java的重写(overwrite)。可以通过成员访问其嵌套的匿名结构体,使用其调用时则会调用到接收器为嵌套类型的结构体。
type Person struct {
name string
}
func (receiver *Person) sayHello() {
println("我是Person" + receiver.name)
}
type Student struct {
*Person
studentId int
}
func (receiver *Student) sayHello() {
println("我是Student" + receiver.name)
}
func main() {
student := Student{Person: &Person{name: "张三"}, studentId: 20}
//我是Student张三
student.sayHello()
//我是Person张三
student.Person.sayHello()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
到这里你应该适应这样一个思维: 所谓面相,无非一个复合的数据类型,和一些参数是该类型的函数。
多态: Golang的多态只能通过接口实现。接口的含义是,保证可以通过某种方式输入输出。
只关心参数可以调用到特定函数,而不关心具体具体结构体类型,就使用接口类型。
Golang的接口实现是隐式的,当存某个结构体,以其为接收器的函数涵盖了接口所有的函数,就视为该结构体实现了该接口。
type SayHelloAble interface {
sayHello()
}
type Person struct {
name string
}
func (receiver *Person) sayHello() {
println("我是Person" + receiver.name)
}
type Student struct {
*Person
studentId int
}
func (receiver *Student) sayHello() {
println("我是Student"+receiver.name, "我的ID是"+strconv.Itoa(receiver.studentId))
}
type Teacher struct {
*Person
major string
}
func (receiver *Teacher) sayHello() {
println("Teacher"+receiver.name, "我的专业是"+receiver.major)
}
func main() {
var sa SayHelloAble
t := &Teacher{
Person: &Person{name: "张三"},
major: "语文",
}
sa = t
//Teacher张三 我的专业是语文
sa.sayHello()
s := &Student{
Person: &Person{name: "李四"},
studentId: 20,
}
sa = s
//我是Student李四 我的ID是20
sa.sayHello()
sa = s.Person
//我是Person李四
sa.sayHello()
}
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