面向對象編程 —— java實現函數求導

首先聲明一點,本文主要介紹的是面向對象(OO)的思想,順便談下函數式編程,而不是教你如何準確地、科學地用java求出函數在一點的導數。html

 

1、引子

 

def d(f) :
    def calc(x) :
        dx = 0.000001  # 表示無窮小的Δx
        return (f(x+dx) - f(x)) / dx  # 計算斜率。注意,此處引用了外層做用域的變量 f
    return calc  # 此處用函數做爲返回值(也就是函數 f 的導數)
# 計算二次函數 f(x) = x2 + x + 1的導數
f = lambda x : x**2 + x + 1  # 先把二次函數用代碼表達出來
f1 = d(f)# 這個f1 就是 f 的一階導數啦。注意,導數依然是個函數
# 計算x=3的斜率
f1(3)
# 二階導數
f2 = d(f1)

首先,直接上一段python代碼,請你們先分析下上面代碼是用什麼方法求導的。請不要被這段代碼嚇到,你無需糾結它的語法,只要明白它的求導思路。java

以上代碼引用自《爲啥俺推薦 Python[4]:做爲函數式編程語言的 Python》,這篇博客是促使我寫篇文章的主要緣由。python

博主說「若是不用 FP,改用 OOP,上述需求該如何實現?俺以爲吧,用 OOP 來求導,這代碼寫起來多半是又醜又臭。」編程

我將信將疑,因而就用面向對象的java試了試,最後也沒多少代碼。若是用java8或之後版本,代碼更少。api

請你們思考一個問題,如何用面向對象的思路改寫這個程序。請先好好思考,嘗試編個程序再繼續往下看。oracle

考慮到看到這個標題進來的同窗大可能是學過java的,下面我用java,用面向對象的思路一步步分析這個問題。app

 

2、求導

 

文章開頭我已近聲明過了,本文不是來討論數學的,求導只是我用來講明面向對象的一個例子。編程語言

若是你已經忘了開頭那段代碼的求導思路,請回頭再看看,看看用python是如何求導的。ide

相信你只要據說過求導,確定一眼就看出開頭那段代碼是用導數定義求導的。函數式編程

代碼中只是將無窮小Δx粗略地算作一個較小的值0.000001。

 

3、最初的想法

 

//自定義函數
public class Function {
    //函數:f(x) = 3x^3 + 2x^2 + x + 1
    public double f(double x) {
        return 3 * x * x * x + 2 * x * x + x + 1;
    }
}
//一元函數導函數
public class DerivedFunction {
    //表示無窮小的Δx
    private static final double DELTA_X = 0.000001;
    //待求導的函數
    private Function function;

    public DerivedFunction(Function function) {
        this.function = function;
    }

    /**
     * 獲取function在點x處的導數
     * @param x 待求導的點
     * @return 導數
     */
    public double get(double x) {
        return (function.f(x + DELTA_X) - function.f(x)) / DELTA_X;
    }
}
public class Main {
    public static void main(String[] args) {
        //一階導函數
        DerivedFunction derivative = new DerivedFunction(new Function());
        //打印函數在x=2處的一階導數
        System.out.println(derivative.get(2));
    }
}

先聲明一點,考慮到博客篇幅,我使用了不規範的代碼註釋,但願你們不要被我誤導。

我想只要你們好好思考了,應該至少會想到這步吧。代碼我就不解釋了,我只是用java改寫了文章開頭的那段python代碼,作了一個簡單的翻譯工做。再請你們考慮下以上代碼的問題。

剛開始,我思考這個問題想到的是建一個名爲Function的類,類中有一個名爲f的方法。但考慮到要每次要求新的函數導數時就得更改這個f方法的實現,明顯不利於擴展,這違背了開閉原則

估計有的同窗沒聽過這個詞,我就解釋下:」對象(類,模塊,函數等)應對擴展開放,但對修改封閉「。

因而我就沒繼續寫下去,但爲了讓你們直觀的感覺到這個想法,我寫這篇博客時就實現了一下這個想法。

請你們思考一下如何重構代碼以解決擴展性問題。

 

4、初步的想法

 

估計學過面向對象的同窗會想到把Function類改爲接口或抽象類,之後每次添加新的函數時只要重寫這個接口或抽象類中的f方法,這就是面向接口編程,符合依賴反轉原則,下面的代碼就是這麼作的。

再聲明一點,考慮到篇幅的問題,後面的代碼我會省去與以前代碼重複的註釋,有不明白的地方還請看看上一個想法中的代碼。

//一元函數
public interface Function {
    double f(double x);
}
//自定義的函數
public class MyFunction implements Function {
    @Override
    public double f(double x) {
        return 3 * x * x * x + 2 * x * x + x + 1;
    }
}
public class DerivedFunction {
    private static final double DELTA_X = 0.000001;
    private Function function;

    public DerivedFunction(Function function) {
        this.function = function;
    }

    public double get(double x) {
        return (function.f(x + DELTA_X) - function.f(x)) / DELTA_X;
    }
}
public class Main {
    public static void main(String[] args) {
        //一階導函數:f'(x) = 9x^2 + 4x + 1
        DerivedFunction derivative = new DerivedFunction(new MyFunction());
        System.out.println(derivative.get(2));
    }
}

我想認真看的同窗可能會發現一個問題,個人翻譯作的還不到位,開頭那段python代碼還能夠輕鬆地求出二階導函數(導數的導數),而個人代碼卻不行。

其實只要稍微修改以上代碼的一個地方就能夠輕鬆實現求二階導,請再思考片刻。

 

5、後來的想法

 

當我寫出上面的代碼時,我感受徹底能夠否認「用 OOP 來求導,這代碼寫起來多半是又醜又臭」的觀點。但還不能求二階導,我有點不甘心。

因而我就動筆,列了一下用定義求一階導和求二階導的式子,想了想兩個式子的區別與聯繫,忽然想到導函數也是函數。

DerivedFunction的get方法和Function的f方法的參數和返回值同樣,DerivedFunction能夠實現Function接口,因而產生了下面的代碼。

public interface Function {
    double f(double x);
}
public class DerivedFunction implements Function {
    private static final double DELTA_X = 0.000001;
    private Function function;

    public DerivedFunction(Function function) {
        this.function = function;
    }

    @Override
    public double f(double x) {
        return (function.f(x + DELTA_X) - function.f(x)) / DELTA_X;
    }
}
public class Main {
    public static void main(String[] args) {
        Function f1 = new DerivedFunction(new Function() {
            @Override
            public double f(double x) {
                return 3 * x * x * x + 2 * x * x + x + 1;
            }
        });
        System.out.println(f1.f(2));
        //二階導函數:f''(x) = 18x + 4
        Function f2 = new DerivedFunction(f1);
        //打印函數f(x) = 3x^3 + 2x^2 + x + 1在x=2處的二階導數
        System.out.println(f2.f(2));
    }
}

考慮到有的同窗沒學過java8或以上版本,以上代碼沒有用到java8函數式編程的新特性。 

若是你接觸過java8,請考慮如何改寫以上代碼,使其更簡潔。

 

6、最後的想法

 

public class DerivedFunction implements Function<Double, Double> {
    private static final double DELTA_X = 0.000001;
    private Function<Double, Double> function;

    public DerivedFunction(Function<Double, Double> function) {
        this.function = function;
    }

    @Override
    public Double apply(Double x) {
        return (function.apply(x + DELTA_X) - function.apply(x)) / DELTA_X;
    }
}
public class Main {
    public static void main(String[] args) {
        //打印函數在x=2處的二階導
        System.out.println(new DerivedFunction(new DerivedFunction(x -> 3 * x * x * x + 2 * x * x + x + 1)).apply(2.0));
    }
}

以前幾個想法爲了擴展Function接口,使用了外部類、匿名類的方式,其實也能夠用內部類。而這在這裏,我用了lambda表達式,是否是更簡潔了。

這裏用的Function接口用的是jdk自帶的,咱們不須要本身定義了。由於這是一個函數式接口,咱們能夠用lambda方便地實現。後來發現,其實這裏用UnaryOperator這個接口更恰當。

如今你們有沒有發現,用java、用OOP也能夠很是簡潔地實現求導,並不比開頭的那段python代碼麻煩不少。

 

7、編程範式

 

在我看來,編程範式簡單來講就是編程的一種模式,一種風格。

我先介紹其中的三個,你差很少就知道它的含義了。

 

7.1 面向對象程序設計(OOP)

看到這裏的同窗應該對面向對象有了更直觀的認識。在面向對象編程中,萬物皆對象,抽象出類的概念。基本特性是封裝、繼承、多態,認識不深的同窗能夠再去我以前的代碼中找找這三個特性。

我以前還介紹了面向對象的幾個原則:開閉原則依賴反轉原則。其餘還有單一職責原則里氏替換原則接口隔離原則。這是面向對象的5個基本原則,合稱SOLID

 

7.2 函數編程語言(FP)

本文開頭那段代碼用的就是python函數式編程的語法,後來我又用java8函數式編程的語法翻譯了這段代碼。

相信你已經直觀地感覺到它的簡潔,以函數爲核心,幾行代碼就解決了求導的問題。

 

7.3 過程式編程(Procedural programming)

大概學過編程都學過C,C語言就是一種過程式編程語言。在我看來,過程式編程大概就是爲了完成一個需求,像記流水賬同樣,平鋪直敘下去。 

       

8、結尾

 

因爲本人初學java,目前只能想到這麼多。若是你們有更好的想法或者覺的我上面說的有問題,歡迎評論,望各位不吝賜教。

這是個人第一篇技術博客,希望我說清楚了面向對象。若是對你有幫助,請點個贊或者評論下,給我點繼續創做的動力。

相關文章
相關標籤/搜索