前言
谈到面对对象(OOP),类,对象,封装,继承,抽象,接口,多态,组合是绕不过的点。本文会将Java与Golang的OOP进行简单对比,希望能帮到大家!
1.类和对象
在面对对象中,类被用来描述一个事物,一个类包含属性及其对应的方法。对于Java,它有比较完善的类机制,既有实体类也有抽象类,实体类可以被实例化,而抽象类不可以,
而在Golang中,并没有类这种概念,跟它比较贴近的是结构体(struct),具体如下:
type Rectangle struct {
width int
height int
}
func (r Rectangle) setWidth() {
r.width = 100
}
func (r *Rectangle) setHeight() {
r.height = 100
}
func main() {
// 类似于实例化一个对象
rec := Rectangle{width: 1, height: 1}
rec.setWidth()
fmt.Println(rec.width) // 仍然是1
rec.setHeight()
fmt.Println(rec.height) // 变为100
}
对于上面的代码,你可以简单将Rectangle看成一个类,而width和height是其属性,setWidth和setHeight是其对应的方法,我一般将其称作结构体方法。
不过Golang的结构体方法比较有特色,它包含两种,一种是带指针的,一种是不带指针的,它们的区别主要是:不带指针的方法(setWidth),传递的参数r其实是Rectangle的一份拷贝,所以即使在方法里面修改了width的值,原有的结构体并不受影响;而带指针的方法则相反。
官方文档也提到了这两者的区别:
- 1.如果你需要在方法/函数内改变
Rectangle的值,则必须声明一个指针方法。 - 2.如果你的
Rectangle结构体数据较大,比如有100个字段,如果使用不带指针的方法,则每一次都需要对其进行拷贝,内存消耗较大,此时建议使用带指针的方法。 - 3.如果有部分方法是带指针的,则为了代码统一,建议其他方法也写成带指针的。
2.封装
关于封装,其实就是将一个事物的属性和方法封装在一个类中,体现的是一种高内聚,低耦合的思想。在封装中,比较核心的是访问控制符,即哪些属性/方法是可以访问的,哪些是不可以访问的。
在Java中,访问控制符是显式定义的,有四种,用public,private,protected,default四个关键字表示。
而在Go中,访问控制符是隐式定义的,如果这个变量首字母小写,则表示不可以被外部访问,如果是大写,则可以被外部访问,如下:
// go6_oop/person/person.go
package person
type Person struct {
// 名称和年龄可暴露,邮箱不可以
Name string
Age int
email string
}
// go6_oop/encapsulation_lab.go
package main
import (
"fmt"
"go6_oop/person"
)
func main() {
// 正常打印
r := person.Person{Name: "Test", Age: 18}
fmt.Println(r)
// 加入email并运行,提示:unknown field 'email' in struct literal of type person.Person
// r := person.Person{Name: "Test", Age: 18, email: ""}
// fmt.Println(r)
}
3.继承和组合
继承和组合都是实现代码复用的方式,继承代表的是一种 is-a 的概念,而组合代表的是一种 has-a 的概念。
在Java中,既有继承,也有组合;而在Golang中,只有组合,没有继承。
如果以交通工具,汽车,轮胎为例,Java的实现一般为:
// 定义一个交通工具抽象类
public abstract class Verhicle{
}
// 定义一个轮胎类
class Tire{
}
// 定义一个汽车类,包含轮胎类,并继承交通工具抽象类
class Car extends Verhicle{
private Tire t=new Tire();
}
而Golang的实现其实也大同小异,所以尽管没有继承,但通过某种方式,也可以实现出继承的效果:
// 定义一个交通工具接口 (类似抽象类)
type Vehicle interface {
Run(key bool) error
}
// 定义一个轮胎结构体
type Tire struct {
Radius float32 // 半径
Num int // 数量
}
// 定义一个汽车结构体,包含轮胎,并实现交通工具接口
type Car struct {
// 组合
tire Tire
}
func (c *Car) Run(key bool) error {
if key {
fmt.Println("Run...")
return nil
}
return errors.New("no key")
}
// 测试
func main() {
// 这两行代码有点类似Java的 Animal a = new Dog()
var v Vehicle
v = &Car{}
err := v.Run(true)
if err != nil {
panic(err)
}
}
4.抽象类/接口
在Java中,抽象类和接口的区别,有一张图解释得特别好:
注:该图来源于知乎的Java基础之抽象类和接口的区别,侵权删。
不过个人感觉从逻辑层面而言,抽象类和接口的区别并不大,因为都表达了一种抽象的理念,它们更多的是技术上的区别。
也不知道是不是因为这个原因,在Golang中,也没有抽象类的概念,只有接口。而且Golang的接口也不需要像Java那样需要有一个implement关键字显式声明,只要有一个结构体实现了一个接口中的所有方法,就认为它实现了这个接口,如上文代码。
5.多态
什么是多态呢?根据百度百科,在面向对象语言中,接口的多种不同的实现方式即为多态。
在Java中,多态可以通过继承和接口实现;而在Golang中,使用接口即可实现多态,或者说Golang天然就是支持多态的。
如果在上文的代码中,加入一个火车结构体(Train),并实现 Vehicle 接口,其实就已经实现了多态(Vehicle接口有Car和Train多种实现方式),所以多态其实在Golang中是随处可见。
总结
- 1.Golang并没有类的概念,与之贴近的是结构体(struct)。
- 2.Golang的访问控制通过变量的首字母是否大写来判断。
- 3.Golang没有继承,只有组合。
- 4.Golang没有抽象类,只有接口,抽象类和接口本身的差距也不是很大。
- 5.Golang天然支持多态,它的多态通过接口来实现。
最后,从上面可以看到,Golang的OOP其实删减了很多概念,所以看起来会比Java简洁得多(毕竟没有了类和继承,也就没有构造器方法,静态方法或者类方法)。不过每种语言都有其适用场景以及编码套路,并不是说这样就是最好的。
补充
指针
指针其实跟变量一样,只不过普通变量存放的是数值,而指针变量存放的是数值的内存地址。
其中&符号表示对变量取地址,得到一个指针;*符号有两种用法,如果*后面跟的是指针,则表示对指针取值,即得到指针指向的值;如果如果*后面跟的是类型,则表示一个指向该类型的指针。
Go文档的一个栗子:
func main() {
i := 42 // 初始化一个值
p := &i // &i表示对变量i取地址,并把地址赋给p,所以p是一个指针,指向i的值
fmt.Println("pointer", p) // pointer 0xc000020078
fmt.Println("value", *p) // *表示对指针取值,即得到p指向的值,为42
*p = 21 // 通过指针赋值
fmt.Println(i) // 21
var k *int // 这里的k是一个指针!!!
}
注:Golang的指针不能进行计算。指针计算其实就是指针加减,在C语言中,是允许这样操作的,比如我们知道c语言中的数组名其实就是该数组的首地址,通过下标取值其实就是首地址的偏移,即加减。
参考
1.Should I define methods on values or pointers?
2.go struct 方法使用指针与不使用指针的区别?
3.Java封装的定义和使用
4.Java基础之抽象类和接口的区别
5.搞懂Java继承、覆盖和多态
6.Golang面向对象分析