Java 与 Go:函数

154 阅读15分钟

今天让我们来一起学习函数(在某些语言中也会被称为方法【Method】)。函数是一段被命名的代码块,用于执行特定的任务或完成特定的功能,也是组织和重用代码的基本单位。通过定义和调用函数,你可以将代码分解为更小的逻辑单元,提高代码的可读性、可维护性和重用性。

基本语法

Java

在Java中,方法是类的成员,它们可以被调用以执行相应的操作。

在定义java的语法时,我们需要考虑方法的修饰符、返回类型、方法名、参数列表和方法体。格式通常为

// 修饰符 返回类型 方法名(参数列表) {
//     方法体
//     return 返回值; // 可选,根据返回类型确定是否需要返回值
// }

//这里解释一下修饰符我们通常通过public, private等关键词来
//限定该方法的可调用范围,
//还可以通过static关键词来决定方法属于类还是实例,还能
//通过synchronized关键字来处理并发。

简单举个例子

public class MyClass {
    // 定义一个方法,用于计算两个整数的和
    // 由于我加了public static这两个修饰符
    // 外部函数直接可以通过类进行调用
    // int res = MyClass.add(1, 2);
    public static int add(int a, int b) {
        int sum = a + b;
        return sum;
    }

    // 定义一个无返回值的方法,用于打印输出信息
    public static void printMessage(String message) {
        System.out.println(message);
    }

    // 主方法,程序入口
    public static void main(String[] args) {
        int result = add(3, 5); // 调用 add 方法并将结果赋值给 result 变量
        System.out.println("The sum is: " + result); // 打印结果

        printMessage("Hello, World!"); // 调用 printMessage 方法打印消息
    }
}
//要注意静态方法不能调用non-static(普通)的方法
//但是普通方法确实可以调用静态方法

Go

在Go语言中,没有像Java中的 public、private、protected 这样的访问修饰符。但是,Go语言通过首字母的大小写来确定标识符(变量、常量、函数、结构体等)的可见性和访问权限。

  • 首字母大写的标识符:表示该标识符是可导出的(Public)。可导出的标识符可以在包外部被访问和使用。

  • 首字母小写的标识符:表示该标识符是不可导出的(Private)。不可导出的标识符只能在定义它们的包内部使用,无法在包外部被访问和使用。

例如,

package mypackage

// MyFunction 是一个可导出的函数,可以在包外部被调用。
func MyFunction() {
   //↑ 首字母大写
}

// myFunction 是一个不可导出的函数,只能在定义它的包内部使用。
func myFunction() {
   //↑ 首字母小写
}

那让我们来详细看看Go语言如何定义函数

func functionName(parameterList) returnType {
    // 函数体
    // 执行特定任务或完成特定功能
    return value // 可选,根据 returnType 确定是否需要返回值
}

//func 关键字用于声明一个函数。
//functionName 是函数的名称,用于调用该函数。
//parameterList 是函数的参数列表,每个参数由参数名和参数类型组成,多个参数之间使用逗号分隔。
//returnType 是函数返回值的类型。如果函数没有返回值,则可以省略 returnType 部分。
//return 语句用于从函数中返回一个值,value 是要返回的值。

来些例子理解起来更好

//无参无返回值的函数
func greet() {
    fmt.Println("Hello, World!")
}

//单参数无返回值的函数
func greet(name string) {
    fmt.Println("Hello, ", name)
}

//无参有返回值的函数
func getMessage() string {
    return "Hello, World!"
}

//参数双返回值的函数
func divide(dividend, divisor float64) (float64, error) {
    if divisor == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    quotient := dividend / divisor
    return quotient, nil
}

重写(Override)和重载(Overload)

在面向对象编程中,二者是非常重要的概念。我们先来聊一聊重载

函数的重载(Overload)

函数的重载指的是在同一个类中定义多个同名的方法,但它们的参数列表必须不同(参数的类型、个数、顺序),以便根据调用时的参数类型来确定调用哪个方法。

class Example {
    void display() {
        System.out.println("No parameters");
    }

    void display(int x) {
        System.out.println("Parameter: " + x);
    }

    void display(String str) {
        System.out.println("Parameter: " + str);
    }
}

然而在Go语言中,不能定义同名但参数列表不同的方法。如果需要处理不同类型的参数,可以使用接口或者通过不同的函数名进行区分。

package main

import "fmt"

type MyType struct{}

func (m *MyType) displayString(str string) {
    fmt.Println("String:", str)
}

func (m *MyType) displayInt(num int) {
    fmt.Println("Int:", num)
}

func main() {
    obj := &MyType{}
    obj.displayString("Hello") // 输出 "String: Hello"
    obj.displayInt(10)         // 输出 "Int: 10"
}

方法重写(Override)

函数的重写指的是在子类中重新定义父类中已经定义的方法。子类中的方法和父类中的方法具有相同的名称、参数列表和返回类型,即子类中的方法覆盖了父类中的同名方法。我们也通常在重写方法前加上@Override注解。

class Parent {
    void display() {
        System.out.println("Parent class method");
    }
}

class Child extends Parent {
    @Override
    void display() {
        System.out.println("Child class method");
    }
}
//Child 类重写了 Parent 类中的 display 方法。

在 Go 语言中,方法重写的概念与 Java 等其他面向对象语言略有不同。Go 语言中的结构体内嵌时可以使用匿名字段,这种方式可以实现类似于方法重写的效果,但它不是真正意义上的方法重写,而是方法继承和覆盖。

package main

import "fmt"

type Parent struct{}

func (p *Parent) display() {
    fmt.Println("Parent method")
}

type Child struct {
    Parent // 匿名字段,继承了 Parent 类型的方法
}

func (c *Child) display() {
    fmt.Println("Child method")
}

func main() {
    parent := &Parent{}
    parent.display() // 输出 "Parent method"

    child := &Child{}
    child.display() // 输出 "Child method"
}

在上面的代码中,Child 结构体嵌入了 Parent 结构体,这样 Child 类型就继承了 Parent 类型的所有方法。当 Child 类型定义了与 Parent 类型相同的方法时,它们会覆盖 Parent 类型中的方法,从而实现了方法重写的效果。

需要注意的是,Go 语言中的方法重写是通过方法覆盖来实现的,而不是通过继承和重写的方式。因此,虽然通过匿名字段可以实现类似于方法重写的效果,但它们之间的实现机制有所不同。

错误、异常处理

异常(Exception)是指在程序运行过程中出现的不正常情况或错误条件。异常表示了程序在执行过程中遇到的问题,它可能导致程序无法继续正常执行,从而需要采取适当的措施来处理。

Java

在Java中,异常处理是通过使用 try-catch 块来实现的。通过捕获异常,你可以在发生异常时采取适当的措施,以确保程序不会意外终止或导致错误结果。然而我们也可以通过在方法声明中使用 throws 关键字来将异常传递给调用者。当一个方法可能会抛出异常,但不想在方法内部进行处理时,可以使用 throws 关键字在方法签名中声明该异常,通知调用者可能会抛出这个异常,由调用者来处理。

import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class FileHandler {
    // 读取文件内容并返回
    public static String readFile(String fileName) throws FileNotFoundException {
        FileInputStream file = new FileInputStream(fileName);
        // 读取文件内容的逻辑
        return "File content"; // 假设返回文件内容
    }

    public static void main(String[] args) {
        try {
            String content = readFile("file.txt");
            System.out.println("File content: " + content);
        } catch (FileNotFoundException e) {
            System.out.println("File not found: " + e.getMessage());
        }
    }
}

Go

在Go语言中,错误处理是通过返回一个错误值(通常是 error 类型)来实现的。Go语言推崇的错误处理方式是在函数中返回一个错误值,并在调用该函数的地方对错误进行处理。这种机制简洁明了,符合Go语言的设计哲学。

func divide(a, b float64) (float64, error) {//声明该方法可能返回错误
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}
result, err := divide(10, 0)
if err != nil {
    fmt.Println("Error:", err)
} else {
    fmt.Println("Result:", result)
}
/**在这个例子中,我们调用了 divide 函数,并检查了返回的错误值。
如果 err 不为 nil,说明函数执行过程中出现了错误,
我们可以根据需要进行错误处理。

通过这种方式,Go语言的错误处理简单直接,但同时也非常有效。
它鼓励开发者在代码中显式处理错误,
避免了异常处理可能带来的性能损耗和代码复杂度增加。
*/

除了err := fmt.Errorf("division by zero"), 我们还可以用err := errors.New("division by zero")

不仅如此,Go语言还支持异常中断程序,使用panic函数来使发生异常的程序终止。 两种方式触发

  • 手动调用panic函数
package main

import "fmt"

func main() {
    fmt.Println("Start")

    // 发生panic
    panic("Something went wrong!")

    fmt.Println("End") // 这行代码不会执行
}

//一旦发生 panic,程序会立即停止执行当前函数的剩余代码,
//并开始执行该函数的 defer 延迟函数(接下来会讲),
//然后返回该函数的调用者
  • 某些错误会自动触发
    • 数组越界
    • 空指针引用
    • 切片越界
    • 除以0

panic可以沿着调用堆栈向外传递,而我们可以通过defer和recover来处理panic异常。

defer 用于延迟(defer)函数的执行,使得函数可以在当前函数执行完成后再执行。defer 关键字经常被用于资源释放、错误处理等场景,使得代码更加清晰和简洁。(可以理解成压栈。如果注册多个defer,后进先出)

recover 用于恢复程序的控制流,应该在 defer 延迟函数中调用,用于恢复由于 panic 导致的异常情况。如果没有发生 panic 或者发生 panic 的函数没有被 defer 延迟函数捕获,recover 函数会返回 nil。

package main

import "fmt"

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()

    fmt.Println("Start")

    // 发生panic
    panic("Something went wrong!")

    fmt.Println("End") // 这行代码不会执行
}

在上面的示例中,当发生 panic 时,defer 函数中的 recover 将捕获 panic,并打印出错误消息,然后程序将正常终止,而不是继续抛出异常。

总之,panic 和 recover 是 Go 语言中用于处理异常和错误的两个关键字。它们可以用来控制程序的流程,处理一些意外情况,保证程序的稳定性。但在一般的程序中,应该避免滥用 panic 和 recover,尽量使用错误处理机制来处理可预期的错误。

小结

个人感觉,error更像是Java中的 return Exception 而panic 像是 throws exception

匿名函数与函数式编程

在此之前要先提出一个概念:一等公民

在编程语言中,一等公民(First-class citizens)指的是某种实体(通常是数据、函数或对象)具有以下特性之一或多个特性:

  • 能够被存储在变量中:一等公民可以作为变量的值存储在内存中,因此可以被传递给函数、赋值给其他变量等。

  • 能够作为参数传递给函数:一等公民可以作为函数的参数传递给其他函数,从而可以在函数之间进行传递和操作。

  • 能够作为函数的返回值:一等公民可以作为函数的返回值返回给调用者,因此可以作为函数的结果进行操作。

  • 能够在运行时创建:一等公民可以在程序运行时动态创建和操作,而不需要在编译时确定其结构或类型。

  • 编程语言中的一等公民通常是指某种特定类型的实体,如函数、对象、数组等。例如,在函数式编程语言中,函数通常被视为一等公民,因为它们可以被存储在变量中、作为参数传递给其他函数,甚至作为函数的返回值。

具有一等公民特性的实体可以更灵活地在程序中使用和操作,这有助于编写更简洁、灵活和可维护的代码。一等公民的概念对于理解编程语言的设计和功能非常重要,因为它们定义了编程语言中的基本元素和操作。

Java

在很久很久以前,Java的函数并不是一等公民(现在也不能说是完全是),直到Java8引入了函数式编程,也就是我们熟知的lambda表达式以及函数式接口,使得函数可以作为参数传递给其他函数,也可以作为返回值返回,从而更接近一等公民的概念。

import java.util.function.*;

// 定义一个函数式接口,用于操作两个整数并返回结果
interface Operation {
    int operate(int a, int b);
}

// 定义一个类,用于执行操作
class Calculator {
    // 定义一个方法,接收一个Operation类型的函数作为参数,并执行该函数
    static int calculate(int x, int y, Operation operation) {
        return operation.operate(x, y);
    }
}

public class Main {
    public static void main(String[] args) {
        // 示例1:将Lambda表达式作为参数传入
        int result1 = Calculator.calculate(10, 5, (a, b) -> a + b);
        System.out.println("Result 1: " + result1); // 输出 15

        // 示例2:将方法引用作为参数传入
        int result2 = Calculator.calculate(10, 5, Main::subtract);
        System.out.println("Result 2: " + result2); // 输出 5

        // 示例3:将方法作为返回值返回
        BinaryOperator<Integer> adder = Main::add;
        int result3 = adder.apply(10, 5);
        System.out.println("Result 3: " + result3); // 输出 15
    }

    // 定义一个静态方法,用于加法操作
    static int add(int x, int y) {
        return x + y;
    }

    // 定义一个静态方法,用于减法操作
    static int subtract(int x, int y) {
        return x - y;
    }
}

Go

package main

import "fmt"

// 在 Go 语言中,函数被认为是一种类型。函数类型可以被赋值给变
// 量,也可以作为参数传递给其他函数,或者作为函数的返回值。
type Operation func(int, int) int

// 函数1
func add(x, y int) int {
    return x + y
}

// 函数2
func subtract(x, y int) int {
    return x - y
}

// 接受函数类型作为参数的函数
func calculate(x, y int, operation Operation) int {
    return operation(x, y)
}

func main() {
    // 将函数作为值赋给变量
    var op Operation
    op = add
    fmt.Println(op(3, 5)) // 输出 8

    op = subtract
    fmt.Println(op(10, 5)) // 输出 5

    // 将函数作为参数传递给另一个函数
    fmt.Println(calculate(10, 5, add))        // 输出 15
    fmt.Println(calculate(10, 5, subtract))   // 输出 5

    // 函数作为返回值
    op = getOperation()
    fmt.Println(op(20, 10)) // 输出 10
}

// 返回函数类型的函数
func getOperation() Operation {
    return subtract
}

除此以外,Go语言还可以直接声明匿名函数,而无需提前定义函数名称。匿名函数可以直接作为表达式,也可以赋值给变量,或者作为参数传递给其他函数。

package main

import "fmt"

func main() {
    // 匿名函数作为变量
    add := func(x, y int) int {
        return x + y
    }

    fmt.Println(add(3, 5)) // 输出 8

    // 直接调用匿名函数
    result := func(x, y int) int {
        return x * y
    }(3, 5)

    fmt.Println(result) // 输出 15

    // 闭包示例
    multiplier := func(factor int) func(int) int {
        return func(x int) int {
            return x * factor
        }
    }

    double := multiplier(2)
    fmt.Println(double(5)) // 输出 10
}

上面提到了一个新的概念,闭包,我放到下面来讲

闭包

闭包是一种函数,它可以捕获其外部作用域中的变量,并将其保存在自己的闭包中。闭包在许多编程语言中都是一种强大的概念,它可以帮助我们编写更灵活、更模块化的代码。

封装

闭包可以用于封装变量和函数,从而隐藏内部实现的细节,只暴露必要的接口。这种封装机制可以帮助我们编写更清晰、更易于维护的代码。

func Counter() func() int {
    count := 0 //对下面的函数而言,这就是外部变量
    return func() int {
        count++
        return count
    }
}

func main() {
    counter := Counter()
    fmt.Println(counter()) // 输出 1
    fmt.Println(counter()) // 输出 2
    fmt.Println(counter()) // 输出 3
}

/**
Counter函数返回了一个闭包,该闭包会捕获外部作用域中的 
count 变量,并且在每次调用时增加计数器的值。
*/

延迟执行

这个可以参考之前提到的defer函数

回调函数

闭包还可以用作回调函数。回调函数是一种将函数作为参数传递给其他函数,并在特定事件发生时调用的技术。闭包可以捕获外部作用域的变量,因此可以在回调函数中访问外部变量的值。

func Process(arr []int, callback func(int) int) {
    for _, v := range arr {
        fmt.Println(callback(v))
    }
}

func main() {
    arr := []int{1, 2, 3, 4, 5}
    double := func(x int) int {
        return x * 2
    }
    Process(arr, double)
}

Process函数接受一个切片和一个回调函数作为参数,并且在切片的每个元素上调用回调函数。这样可以使得 Process 函数更加灵活,可以对切片的每个元素进行不同的处理。

总之,闭包是一种强大的编程概念,它可以帮助我们实现封装、延迟执行、回调函数等功能,从而编写更加灵活、模块化的代码。在上述案例中都是使用Go为大家演示,实际上Java使用lambda表达式也可以达到类似的效果。

总结

本文并未提及函数在并发上的支持,这留到后面并发的专题去讲。总结一下,二者作为面向对象编程的语言,在函数的设计思想上还是很相近的(写法上确实很不一样),Java和Go语言都允许函数作为一等公民,但Go语言更直接支持闭包和匿名函数,简化并发编程和函数式编程。Java在函数式编程方面引入Lambda表达式和函数式接口,但相对复杂。

需要免费使用IDEA的小伙伴可以关注公众号【AIGoland之星】回复【idea】领取最新且免费的激活工具。