在Java中理解函数对象或者叫functor的帮助

31

有人可以解释一下什么是函数对象,并且提供一个简单的例子吗?

4个回答

50
一个函数对象就是这样的,它既是一个对象又是一个函数。
顺便说一下:把函数对象称为“functor”是对该术语的严重滥用: 在数学中,“functors”是一个不同的概念,并在计算机科学中起着直接作用(请参见“Haskell Functors”)。该术语在 ML 中也有略微不同的用法,因此除非您正在使用Java实现其中一个概念(您可以!),否则请停止使用此术语。这使得简单的事情变得复杂。
回到答案: Java没有“一等函数”,也就是说,您不能将一个函数作为参数传递给另一个函数。从多个层面上来看都是如此,从语法上、字节码表示上以及类型系统上都是如此,缺乏“函数构造器”。
换句话说,您无法编写像这样的代码:
 public static void tentimes(Function f){
    for(int i = 0; i < 10; i++)
       f();
 }
 ...
 public static void main{
    ...
    tentimes(System.out.println("hello"));
    ...
 }

这真的很烦人,因为我们想要做一些类似于图形用户界面库的事情,其中你可以将一个“回调”函数与单击按钮关联起来。

那么我们该怎么办呢?

那么,通用解决方案(由其他发帖者讨论)是定义一个具有单个方法的接口,我们可以调用它。例如,Java经常使用一个名为Runnable的接口来处理这些事情,它看起来像:

public interface Runnable{
    public void run();
}

现在,我们可以重新编写上面的示例:

public static void tentimes(Runnable r){
    for(int i = 0; i < 10; i++)
       r.run();
}
...
public class PrintHello implements Runnable{
    public void run{
       System.out.println("hello")
    }
}
---
public static void main{
    ...
    tentimes(new PrintHello());
    ...
 }
显然,这个例子是人为制造的。我们可以使用匿名内部类使代码更清晰一些,但这只是一个大致的想法。
这里的问题在于:Runnable 只适用于不接受任何参数且没有返回有用信息的函数,因此你最终会为每个任务定义一个新接口,例如 Mohammad Faisal 回答中的 Comparator 接口。提供更通用的方法,并且采用语法更简洁的方法是 Java 8 的一个主要目标(即 Java 的下一个版本),并且这也是 Java 7 强烈推动纳入的功能。这被称为“Lambda”,源于 Lambda Calculus 中的函数抽象机制。Lambda Calculus 可能是最古老的编程语言之一,也是计算机科学的理论基础之一。当 Alonzo Church(计算机科学的主要创始人之一)发明它时,他使用希腊字母 Lambda 来表示函数,因此得名。
其他语言(包括函数式语言 Lisp、ML、Haskell、Erlang 等)、大多数主要的动态语言(Python、Ruby、JavaScript 等)和其他应用程序语言(C#、Scala、Go、D 等)都支持某种形式的“Lambda 字面量”。即使 C++(自 C++11 起)现在也有了它们,尽管在这种情况下它们比较复杂,因为 C++ 缺乏自动内存管理,并且不会为您保存堆栈帧。

4
值得一提的是Callable及其方法call(),它会返回某些内容(但仍不需要参数)。 - Paul Bellora
3
这需要很快更新。Java 8将支持一级函数。语法看起来很像C#。 - Darrel Hoffman

16

一个函数对象是一个可以作为函数使用的对象。

Java没有函数对象,因为在Java中函数不是一等公民对象。

但是你可以用接口来近似它们,例如Command对象:

public interface Command {
    void execute(Object [] parameters); 
}

更新于2017年3月18日:

自我最初撰写此文以来,JDK 8已经添加了lambda表达式。 java.util.function 包中有几个有用的接口。


1
+1,而Guava的Function则是另一种近似方法。 - Paul Bellora

8
从每次检查到Functor,再到Java 8的Lambda表达式(有点像)
问题:
考虑以下这个示例类,它将一个{{link2:Appendable}}适配为{{link3:Writer}}:
   import  java.io.Closeable;
   import  java.io.Flushable;
   import  java.io.IOException;
   import  java.io.Writer;
   import  java.util.Objects;
/**
   <P>{@code java WriterForAppendableWChecksInFunc}</P>
 **/
public class WriterForAppendableWChecksInFunc extends Writer  {
   private final Appendable apbl;
   public WriterForAppendableWChecksInFunc(Appendable apbl)  {
      if(apbl == null)  {
         throw  new NullPointerException("apbl");
      }
      this.apbl = apbl;
   }

      //Required functions, but not relevant to this post...START
         public void write(char[] a_c, int i_ndexStart, int i_ndexEndX) throws IOException {
         public Writer append(char c_c) throws IOException {
         public Writer append(CharSequence text) throws IOException {
         public Writer append(CharSequence text, int i_ndexStart, int i_ndexEndX) throws IOException  {
      //Required functions, but not relevant to this post...END

   public void flush() throws IOException {
      if(apbl instanceof Flushable)  {
         ((Flushable)apbl).flush();
      }
   }
   public void close() throws IOException {
      flush();
      if(apbl instanceof Closeable)  {
         ((Closeable)apbl).close();
      }
   }
}

并非所有的“可追加对象(Appendable)”都是“可刷新对象(Flushable)”或“可关闭对象(Closeable)”,但那些是的话,必须同时被关闭和刷新。因此,在每次调用“flush()”和“close()”时,必须检查“Appendable”对象的实际类型,并在确实是该类型时进行强制转换并调用函数。

诚然,这不是最好的示例,因为每个实例只调用一次“close()”,而“flush()”也不一定经常调用。此外,“instanceof”虽然具有反射性,但在这种特定的使用情况下并不太糟糕。尽管如此,“每次需要做其他事情时都必须检查某些内容”的概念确实存在,而在真正重要的情况下避免这些“每次”检查会带来显著的好处。

将所有“重量级”检查移到构造函数中

那么从哪里开始?如何在不损害代码的情况下避免这些检查?

在我们的示例中,最简单的步骤是将所有“instanceof”检查移动到构造函数中。

public class WriterForAppendableWChecksInCnstr extends Writer  {
   private final Appendable apbl;
   private final boolean isFlshbl;
   private final boolean isClsbl;
   public WriterForAppendableWChecksInCnstr(Appendable apbl)  {
      if(apbl == null)  {
         throw  new NullPointerException("apbl");
      }
      this.apbl = apbl;
      isFlshbl = (apbl instanceof Flushable);
      isClsbl = (apbl instanceof Closeable);
   }

         //write and append functions go here...

   public void flush() throws IOException {
      if(isFlshbl)  {
         ((Flushable)apbl).flush();
      }
   }
   public void close() throws IOException {
      flush();
      if(isClsbl)  {
         ((Closeable)apbl).close();
      }
   }
}

既然这些“重型”检查只需要进行一次,那么 flush()close() 只需要进行布尔检查即可。虽然这是一种改进,但如何完全消除这些函数内的检查呢?
如果你能定义一个可以被类存储并由 flush()close() 使用的函数,那就太好了...
public class WriterForAppendableWChecksInCnstr extends Writer  {
   private final Appendable apbl;
   private final FlushableFunction flshblFunc;  //If only!
   private final CloseableFunction clsblFunc;   //If only!
   public WriterForAppendableWChecksInCnstr(Appendable apbl)  {
      if(apbl == null)  {
         throw  new NullPointerException("apbl");
      }
      this.apbl = apbl;

      if(apbl instanceof Flushable)  {
         flshblFunc = //The flushable function
      }  else  {
         flshblFunc = //A do-nothing function
      }
      if(apbl instanceof Closeable)  {
         clsblFunc = //The closeable function
      }  else  {
         clsblFunc = //A do-nothing function
      }
   }

          //write and append functions go here...

   public void flush() throws IOException {
      flshblFunc();                             //If only!
   }
   public void close() throws IOException {
      flush();
      clsblFunc();                              //If only!
   }
}

“传递函数是不可能的”...至少在Java 8之前不可能,直到有了Lambda表达式。那么在Java 8之前的版本中该怎么做呢?
函子(functors)。

With a Functor. A Functor is basically a Lambda, but one that is wrapped in an object. While functions cannot be passed into other functions as parameters, objects can. So essentially, Functors and Lambdas are a ways to pass around functions.

So how can we implement a Functor into our writer-adapter? What we know is that close() and flush() are only useful with Closeable and Flushable objects. And that some Appendables are Flushable, some Closeable, some neither, some both.

Therefore, we can store a Flushable and Closeable object at the top of the class:

public class WriterForAppendable extends Writer  {
   private final Appendable apbl;
   private final Flushable  flshbl;
   private final Closeable  clsbl;
   public WriterForAppendable(Appendable apbl)  {
      if(apbl == null)  {
         throw  new NullPointerException("apbl");
      }

      //Avoids instanceof at every call to flush() and close()

      if(apbl instanceof Flushable)  {
         flshbl = apbl;              //This Appendable *is* a Flushable
      }  else  {
         flshbl = //??????           //But what goes here????
      }

      if(apbl instanceof Closeable)  {
         clsbl = apbl;               //This Appendable *is* a Closeable
      }  else  {
         clsbl = //??????            //And here????
      }

      this.apbl = apbl;
   }

          //write and append functions go here...

   public void flush() throws IOException {
      flshbl.flush();
   }
   public void close() throws IOException {
      flush();
      clsbl.close();
   }
}

The "every-time" checks have now been eliminated. But when the Appendable is not a Flushable or not a Closeable, what should be stored?

Do nothing Functors

A do nothing Functor...

class CloseableDoesNothing implements Closeable  {
   public void close() throws IOException  {
   }
}
class FlushableDoesNothing implements Flushable  {
   public void flush() throws IOException  {
   }
}

...which can be implemented as an anonymous inner class:

public WriterForAppendable(Appendable apbl)  {
   if(apbl == null)  {
      throw  new NullPointerException("apbl");
   }
   this.apbl = apbl;

   //Avoids instanceof at every call to flush() and close()
   flshbl = ((apbl instanceof Flushable)
      ?  (Flushable)apbl
      :  new Flushable()  {
            public void flush() throws IOException  {
            }
         });
   clsbl = ((apbl instanceof Closeable)
      ?  (Closeable)apbl
      :  new Closeable()  {
            public void close() throws IOException  {
            }
         });
}

//the rest of the class goes here...

}

To be most efficient, these do-nothing functors should be implemented as static final objects. And with that, here is the final version of our class:

package  xbn.z.xmpl.lang.functor;
   import  java.io.Closeable;
   import  java.io.Flushable;
   import  java.io.IOException;
   import  java.io.Writer;
public class WriterForAppendable extends Writer  {
   private final Appendable apbl;
   private final Flushable  flshbl;
   private final Closeable  clsbl;

   //Do-nothing functors
      private static final Flushable FLUSHABLE_DO_NOTHING = new Flushable()  {
         public void flush() throws IOException  {
         }
      };
      private static final Closeable CLOSEABLE_DO_NOTHING = new Closeable()  {
         public void close() throws IOException  {
         }
      };

   public WriterForAppendable(Appendable apbl)  {
      if(apbl == null)  {
         throw  new NullPointerException("apbl");
      }
      this.apbl = apbl;

      //Avoids instanceof at every call to flush() and close()
      flshbl = ((apbl instanceof Flushable)
         ?  (Flushable)apbl
         :  FLUSHABLE_DO_NOTHING);
      clsbl = ((apbl instanceof Closeable)
         ?  (Closeable)apbl
         :  CLOSEABLE_DO_NOTHING);
   }

   public void write(char[] a_c, int i_ndexStart, int i_ndexEndX) throws IOException {
      apbl.append(String.valueOf(a_c), i_ndexStart, i_ndexEndX);
   }
   public Writer append(char c_c) throws IOException {
      apbl.append(c_c);
      return  this;
   }
   public Writer append(CharSequence c_q) throws IOException {
      apbl.append(c_q);
      return  this;
   }
   public Writer append(CharSequence c_q, int i_ndexStart, int i_ndexEndX) throws IOException  {
      apbl.append(c_q, i_ndexStart, i_ndexEndX);
      return  this;
   }
   public void flush() throws IOException {
      flshbl.flush();
   }
   public void close() throws IOException {
      flush();
      clsbl.close();
   }
}

This particular example comes from this question on stackoverflow. A fully working, and fully-documented version of this example (including a testing function) can be found at the bottom of that question-post (above the answer).

Implementing Functors with an Enum

Leaving our Writer-Appendable example, let's take a look at another way to implement Functors: with an Enum.

As an example, this enum has a move function for each cardinal direction:

public enum CardinalDirection  {
   NORTH(new MoveNorth()),
   SOUTH(new MoveSouth()),
   EAST(new MoveEast()),
   WEST(new MoveWest());

   private final MoveInDirection dirFunc;

   CardinalDirection(MoveInDirection dirFunc)  {
      if(dirFunc == null)  {
         throw  new NullPointerException("dirFunc");
      }
      this.dirFunc = dirFunc;
   }
   public void move(int steps)  {
      dirFunc.move(steps);
   }
}

Its constructor requires a MoveInDirection object (which is an interface, but could also be an abstract class):

interface MoveInDirection  {
   void move(int steps);
}

There are naturally four concrete implementations of this interface, one per direction. Here is a trivial implementation for north:

class MoveNorth implements MoveInDirection  {
   public void move(int steps)  {
      System.out.println("Moved " + steps + " steps north.");
   }
}

Using this Functor is done with this simple call:

CardinalDirection.WEST.move(3);

Which, in our example, outputs this to the console:

Moved 3 steps west.

And here is a full working example:

/**
   <P>Demonstrates a Functor implemented as an Enum.</P>

   <P>{@code java EnumFunctorXmpl}</P>
 **/
public class EnumFunctorXmpl  {
   public static final void main(String[] ignored)  {
       CardinalDirection.WEST.move(3);
       CardinalDirection.NORTH.move(2);
       CardinalDirection.EAST.move(15);
   }
}
enum CardinalDirection  {
   NORTH(new MoveNorth()),
   SOUTH(new MoveSouth()),
   EAST(new MoveEast()),
   WEST(new MoveWest());
   private final MoveInDirection dirFunc;
   CardinalDirection(MoveInDirection dirFunc)  {
      if(dirFunc == null)  {
         throw  new NullPointerException("dirFunc");
      }
      this.dirFunc = dirFunc;
   }
   public void move(int steps)  {
      dirFunc.move(steps);
   }
}
interface MoveInDirection  {
   void move(int steps);
}
class MoveNorth implements MoveInDirection  {
   public void move(int steps)  {
      System.out.println("Moved " + steps + " steps north.");
   }
}
class MoveSouth implements MoveInDirection  {
   public void move(int steps)  {
      System.out.println("Moved " + steps + " steps south.");
   }
}
class MoveEast implements MoveInDirection  {
   public void move(int steps)  {
      System.out.println("Moved " + steps + " steps east.");
   }
}
class MoveWest implements MoveInDirection  {
   public void move(int steps)  {
      System.out.println("Moved " + steps + " steps west.");
   }
}

Output:

[C:\java_code]java EnumFunctorXmpl
Moved 3 steps west.
Moved 2 steps north.
Moved 15 steps east.

I haven't started with Java 8 yet, so I can't write the Lambdas section yet :)


3
这是一篇博客文章 - aliteralmind

4

理解函数应用的概念

f.apply(x)

反转

x.map(f)

x称为函数对象

interface Functor<T> {
    Functor<R> map(Function<T, R> f);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接