首先聲明一點,本文主要介紹的是面向對象(OO)的思想,順便談下函數式編程,而不是教你如何準確地、科學地用java求出函數在一點的導數。html
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
文章開頭我已近聲明過了,本文不是來討論數學的,求導只是我用來講明面向對象的一個例子。編程語言
若是你已經忘了開頭那段代碼的求導思路,請回頭再看看,看看用python是如何求導的。ide
相信你只要據說過求導,確定一眼就看出開頭那段代碼是用導數定義求導的。函數式編程
代碼中只是將無窮小Δx粗略地算作一個較小的值0.000001。
//自定義函數 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方法的實現,明顯不利於擴展,這違背了開閉原則。
估計有的同窗沒聽過這個詞,我就解釋下:」對象(類,模塊,函數等)應對擴展開放,但對修改封閉「。
因而我就沒繼續寫下去,但爲了讓你們直觀的感覺到這個想法,我寫這篇博客時就實現了一下這個想法。
請你們思考一下如何重構代碼以解決擴展性問題。
估計學過面向對象的同窗會想到把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代碼還能夠輕鬆地求出二階導函數(導數的導數),而個人代碼卻不行。
其實只要稍微修改以上代碼的一個地方就能夠輕鬆實現求二階導,請再思考片刻。
當我寫出上面的代碼時,我感受徹底能夠否認「用 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,請考慮如何改寫以上代碼,使其更簡潔。
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代碼麻煩不少。
在我看來,編程範式簡單來講就是編程的一種模式,一種風格。
我先介紹其中的三個,你差很少就知道它的含義了。
看到這裏的同窗應該對面向對象有了更直觀的認識。在面向對象編程中,萬物皆對象,抽象出類的概念。基本特性是封裝、繼承、多態,認識不深的同窗能夠再去我以前的代碼中找找這三個特性。
我以前還介紹了面向對象的幾個原則:開閉原則、依賴反轉原則。其餘還有單一職責原則、里氏替換原則、接口隔離原則。這是面向對象的5個基本原則,合稱SOLID。
本文開頭那段代碼用的就是python函數式編程的語法,後來我又用java8函數式編程的語法翻譯了這段代碼。
相信你已經直觀地感覺到它的簡潔,以函數爲核心,幾行代碼就解決了求導的問題。
大概學過編程都學過C,C語言就是一種過程式編程語言。在我看來,過程式編程大概就是爲了完成一個需求,像記流水賬同樣,平鋪直敘下去。
因爲本人初學java,目前只能想到這麼多。若是你們有更好的想法或者覺的我上面說的有問題,歡迎評論,望各位不吝賜教。
這是個人第一篇技術博客,希望我說清楚了面向對象。若是對你有幫助,請點個贊或者評論下,給我點繼續創做的動力。