diff --git a/core/lib/Drupal/Core/Extension/CachedModuleHandler.php b/core/lib/Drupal/Core/Extension/CachedModuleHandler.php
index 0def7f3..9942cf2 100644
--- a/core/lib/Drupal/Core/Extension/CachedModuleHandler.php
+++ b/core/lib/Drupal/Core/Extension/CachedModuleHandler.php
@@ -123,7 +123,7 @@ protected function getImplementationInfo($hook) {
         // function exists on each request to avoid undefined function errors.
         // Since \Drupal::moduleHandler()->implementsHook() may needlessly try to
         // load the include file again, function_exists() is used directly here.
-        if (!function_exists($module . '_' . $hook)) {
+        if (!is_callable($this->moduleList[$module]->hookPrefix . $hook) && !is_callable($module . '_' . $hook)) {
           // Clear out the stale implementation from the cache and force a cache
           // refresh to forget about no longer existing hook implementations.
           unset($this->implementations[$hook][$module]);
diff --git a/core/lib/Drupal/Core/Extension/Extension.php b/core/lib/Drupal/Core/Extension/Extension.php
index 5c47824..1bcc078 100644
--- a/core/lib/Drupal/Core/Extension/Extension.php
+++ b/core/lib/Drupal/Core/Extension/Extension.php
@@ -74,6 +74,9 @@ class Extension implements \Serializable {
    */
   protected $splFileInfo;
 
+  // @todo Change to protected.
+  public $hookPrefix;
+
   /**
    * Constructs a new Extension object.
    *
@@ -89,6 +92,15 @@ public function __construct($type, $pathname, $filename = NULL) {
     $this->type = $type;
     $this->pathname = $pathname;
     $this->_filename = $filename;
+
+    // @todo Move into ExtensionDiscovery + pass as constructor argument?
+    if (file_exists($this->getPath() . '/lib/Drupal/' . $this->getName() . '/Hook.php')) {
+      $this->hookPrefix = 'Drupal\\' . $this->getName() . '\Hook::';
+    }
+    else {
+      $this->hookPrefix = $this->getName() . '_';
+    }
+
     // Set legacy public properties.
     $this->name = $this->getName();
     $this->uri = $this->getPath() . '/' . $filename;
diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php
index ac48c17..c27f2bc 100644
--- a/core/lib/Drupal/Core/Extension/ModuleHandler.php
+++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php
@@ -244,8 +244,8 @@ public function getHookInfo() {
     // Make sure that the modules are loaded before checking.
     $this->reload();
     foreach ($this->moduleList as $module => $filename) {
-      $function = $module . '_hook_info';
-      if (function_exists($function)) {
+      $function = $this->moduleList[$module]->hookPrefix . 'hook_info';
+      if (is_callable($function)) {
         $result = $function();
         if (isset($result) && is_array($result)) {
           $this->hookInfo = NestedArray::mergeDeep($this->hookInfo, $result);
@@ -276,17 +276,28 @@ public function resetImplementations() {
    * Implements \Drupal\Core\Extension\ModuleHandlerInterface::implementsHook().
    */
   public function implementsHook($module, $hook) {
-    $function = $module . '_' . $hook;
-    if (function_exists($function)) {
-      return TRUE;
+    if (!isset($this->moduleList[$module])) {
+      return FALSE;
+    }
+    $function = $this->moduleList[$module]->hookPrefix . $hook;
+    if (is_callable($function)) {
+      return $function;
+    }
+    // @todo Remove this legacy fallback.
+    if (is_callable($module . '_' . $hook)) {
+      return $module . '_' . $hook;
     }
     // If the hook implementation does not exist, check whether it lives in an
     // optional include file registered via hook_hook_info().
     $hook_info = $this->getHookInfo();
     if (isset($hook_info[$hook]['group'])) {
       $this->loadInclude($module, 'inc', $module . '.' . $hook_info[$hook]['group']);
-      if (function_exists($function)) {
-        return TRUE;
+      if (is_callable($function)) {
+        return $function;
+      }
+      // @todo Remove this legacy fallback.
+      if (is_callable($module . '_' . $hook)) {
+        return $module . '_' . $hook;
       }
     }
     return FALSE;
@@ -296,11 +307,10 @@ public function implementsHook($module, $hook) {
    * Implements \Drupal\Core\Extension\ModuleHandlerInterface::invoke().
    */
   public function invoke($module, $hook, array $args = array()) {
-    if (!$this->implementsHook($module, $hook)) {
+    if (FALSE === $callable = $this->implementsHook($module, $hook)) {
       return;
     }
-    $function = $module . '_' . $hook;
-    return call_user_func_array($function, $args);
+    return call_user_func_array($callable, $args);
   }
 
   /**
@@ -310,9 +320,8 @@ public function invokeAll($hook, array $args = array()) {
     $return = array();
     $implementations = $this->getImplementations($hook);
     foreach ($implementations as $module) {
-      $function = $module . '_' . $hook;
-      if (function_exists($function)) {
-        $result = call_user_func_array($function, $args);
+      if (FALSE !== $callable = $this->implementsHook($module, $hook)) {
+        $result = call_user_func_array($callable, $args);
         if (isset($result) && is_array($result)) {
           $return = NestedArray::mergeDeep($return, $result);
         }
@@ -357,7 +366,7 @@ public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) {
       $modules = $this->getImplementations($hook);
       if (!isset($extra_types)) {
         // For the more common case of a single hook, we do not need to call
-        // function_exists(), since $this->getImplementations() returns only modules with
+        // is_callable(), since $this->getImplementations() returns only modules with
         // implementations.
         foreach ($modules as $module) {
           $this->alterFunctions[$cid][] = $module . '_' . $hook;
@@ -394,14 +403,14 @@ public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) {
         foreach ($modules as $module) {
           // Since $modules is a merged array, for any given module, we do not
           // know whether it has any particular implementation, so we need a
-          // function_exists().
-          $function = $module . '_' . $hook;
-          if (function_exists($function)) {
+          // is_callable().
+          $function = $this->moduleList[$module]->hookPrefix . $hook;
+          if (is_callable($function)) {
             $this->alterFunctions[$cid][] = $function;
           }
           foreach ($extra_types as $extra_type) {
-            $function = $module . '_' . $extra_type . '_alter';
-            if (function_exists($function)) {
+            $function = $this->moduleList[$module]->hookPrefix . $extra_type . '_alter';
+            if (is_callable($function)) {
               $this->alterFunctions[$cid][] = $function;
             }
           }
@@ -458,8 +467,8 @@ protected function getImplementationInfo($hook) {
     foreach ($this->moduleList as $module => $filename) {
       $include_file = isset($hook_info[$hook]['group']) && $this->loadInclude($module, 'inc', $module . '.' . $hook_info[$hook]['group']);
       // Since $this->implementsHook() may needlessly try to load the include
-      // file again, function_exists() is used directly here.
-      if (function_exists($module . '_' . $hook)) {
+      // file again, is_callable() is used directly here.
+      if (is_callable($filename->hookPrefix . $hook) || is_callable($module . '_' . $hook)) {
         $this->implementations[$hook][$module] = $include_file ? $hook_info[$hook]['group'] : FALSE;
       }
     }
diff --git a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php
index 2b6d1df..595b98c 100644
--- a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php
+++ b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php
@@ -187,9 +187,9 @@ public function resetImplementations();
    * @param string $hook
    *   The name of the hook (e.g. "help" or "menu").
    *
-   * @return bool
-   *   TRUE if the module is both installed and enabled, and the hook is
-   *   implemented in that module.
+   * @return string|bool
+   *   A string containing the callable function/method if the module is both
+   *   installed and enabled, and it implements the hook, FALSE otherwise.
    */
   public function implementsHook($module, $hook);
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/Extension/ModuleHookTest.php b/core/modules/system/lib/Drupal/system/Tests/Extension/ModuleHookTest.php
new file mode 100644
index 0000000..1972ea2
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Extension/ModuleHookTest.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Extension\ModuleHookTest.
+ */
+
+namespace Drupal\system\Tests\Extension;
+
+use Drupal\simpletest\DrupalUnitTestBase;
+
+/**
+ * Tests object-oriented hooks.
+ */
+class ModuleHookTest extends DrupalUnitTestBase {
+
+  public static $modules = array('module_test');
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Module hooks',
+      'description' => 'Tests object-oriented hooks.',
+      'group' => 'Extension',
+    );
+  }
+
+  /**
+   * Tests object-oriented Hook class invocations.
+   */
+  public function testHookClass() {
+    $result = $this->moduleHandler()->invoke('module_test', 'test_hook_class');
+    $this->assertIdentical($result, 'Drupal\module_test\Hook::test_hook_class');
+
+    $result = $this->moduleHandler()->invokeAll('test_hook_class');
+    $this->assertIdentical($result, array('Drupal\module_test\Hook::test_hook_class'));
+  }
+
+  /**
+   * Tests procedural hook function invocations.
+   */
+  public function testHookFunction() {
+    $result = $this->moduleHandler()->invoke('module_test', 'test_hook_function');
+    $this->assertIdentical($result, 'module_test_test_hook_function');
+
+    $result = $this->moduleHandler()->invokeAll('test_hook_function');
+    $this->assertIdentical($result, array('module_test_test_hook_function'));
+  }
+
+  /**
+   * Returns the module handler service.
+   *
+   * @return \Drupal\Core\Extension\ModulerHandlerInterface
+   */
+  protected function moduleHandler() {
+    return $this->container->get('module_handler');
+  }
+
+}
diff --git a/core/modules/system/tests/modules/module_test/lib/Drupal/module_test/Hook.php b/core/modules/system/tests/modules/module_test/lib/Drupal/module_test/Hook.php
new file mode 100644
index 0000000..ca21b42
--- /dev/null
+++ b/core/modules/system/tests/modules/module_test/lib/Drupal/module_test/Hook.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @file
+ * module_test hook implementations.
+ */
+
+namespace Drupal\module_test;
+
+class Hook {
+
+  /**
+   * Implements hook_test_hook_class().
+   */
+  public static function test_hook_class() {
+    return __METHOD__;
+  }
+
+}
diff --git a/core/modules/system/tests/modules/module_test/module_test.module b/core/modules/system/tests/modules/module_test/module_test.module
index 0968c68..16c863c 100644
--- a/core/modules/system/tests/modules/module_test/module_test.module
+++ b/core/modules/system/tests/modules/module_test/module_test.module
@@ -140,3 +140,11 @@ function module_test_modules_uninstalled($modules) {
   // can check that the modules were uninstalled in the correct sequence.
   \Drupal::state()->set('module_test.uninstall_order', $modules);
 }
+
+/**
+ * Implements hook_test_hook_function().
+ */
+function module_test_test_hook_function() {
+  return __FUNCTION__;
+}
+
