今天让我们来一起学习函数(在某些语言中也会被称为方法【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】领取最新且免费的激活工具。