diff --git a/core/lib/Drupal/Core/Config/Schema/Mapping.php b/core/lib/Drupal/Core/Config/Schema/Mapping.php
index a85fac4eff976aa22f8d42d81819648cb0d806b5..eed1da87e0c9d86256d51c6f8e1c8f8d186ac942 100644
--- a/core/lib/Drupal/Core/Config/Schema/Mapping.php
+++ b/core/lib/Drupal/Core/Config/Schema/Mapping.php
@@ -88,7 +88,7 @@ public function set($property_name, $value, $notify = TRUE) {
     if ($notify && isset($this->parent)) {
       $this->parent->onChange($this->name);
     }
-    return $property;
+    return $this;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
index 83c9c736961e0219a32c9745d01dd7881e01560d..071f584931ad01f715e71bbcfe1d8c8cd06c2d15 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -385,9 +385,10 @@ protected function getTranslatedField($name, $langcode) {
    * {@inheritdoc}
    */
   public function set($name, $value, $notify = TRUE) {
-    // If default language or an entity key changes we need to react to that.
-    $notify = $name == 'langcode' || in_array($name, $this->getEntityType()->getKeys());
-    $this->get($name)->setValue($value, $notify);
+    // Assign the value on the child and overrule notify such that we get
+    // notified to handle changes afterwards. We can ignore notify as there is
+    // no parent to notify anyway.
+    $this->get($name)->setValue($value, TRUE);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php b/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php
index 0fb2330a9313314f4ea9319446cb74de649f9afd..bbcb0bb90416c39d0289b75ea0b755ae9c40d3ea 100644
--- a/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php
+++ b/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php
@@ -105,7 +105,8 @@ public function set($property_name, $value, $notify = TRUE) {
       throw new \InvalidArgumentException(String::format('Unable to set unknown property @name.', array('@name' => $property_name)));
     }
     // This will throw an exception for unknown fields.
-    return $this->entity->set($property_name, $value, $notify);
+    $this->entity->set($property_name, $value, $notify);
+    return $this;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Field/FieldItemBase.php b/core/lib/Drupal/Core/Field/FieldItemBase.php
index db8601cc45258e5bfb910bbd2ff561c659114193..b411b881700a80bb8503fb48d96acd9e0bb2afe2 100644
--- a/core/lib/Drupal/Core/Field/FieldItemBase.php
+++ b/core/lib/Drupal/Core/Field/FieldItemBase.php
@@ -105,10 +105,7 @@ protected function getSetting($setting_name) {
   }
 
   /**
-   * Overrides \Drupal\Core\TypedData\TypedData::setValue().
-   *
-   * @param array|null $values
-   *   An array of property values.
+   * {@inheritdoc}
    */
   public function setValue($values, $notify = TRUE) {
     // Treat the values as property value of the first property, if no array is
@@ -117,53 +114,39 @@ public function setValue($values, $notify = TRUE) {
       $keys = array_keys($this->definition->getPropertyDefinitions());
       $values = array($keys[0] => $values);
     }
-    $this->values = $values;
-    // Update any existing property objects.
-    foreach ($this->properties as $name => $property) {
-      $value = NULL;
-      if (isset($values[$name])) {
-        $value = $values[$name];
-      }
-      $property->setValue($value, FALSE);
-      unset($this->values[$name]);
-    }
-    // Notify the parent of any changes.
-    if ($notify && isset($this->parent)) {
-      $this->parent->onChange($this->name);
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __get($name) {
-    // There is either a property object or a plain value - possibly for a
-    // not-defined property. If we have a plain value, directly return it.
-    if (isset($this->values[$name])) {
-      return $this->values[$name];
-    }
-    elseif (isset($this->properties[$name])) {
-      return $this->properties[$name]->getValue();
-    }
+    parent::setValue($values, $notify);
   }
 
   /**
    * {@inheritdoc}
+   *
+   * Different to the parent Map class, we avoid creating property objects as
+   * far as possible in order to optimize performance. Thus we just update
+   * $this->values if no property object has been created yet.
    */
-  public function set($property_name, $value, $notify = TRUE) {
+  protected function writePropertyValue($property_name, $value) {
     // For defined properties there is either a property object or a plain
     // value that needs to be updated.
     if (isset($this->properties[$property_name])) {
       $this->properties[$property_name]->setValue($value, FALSE);
-      unset($this->values[$property_name]);
     }
     // Allow setting plain values for not-defined properties also.
     else {
       $this->values[$property_name] = $value;
     }
-    // Directly notify ourselves.
-    if ($notify) {
-      $this->onChange($property_name);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __get($name) {
+    // There is either a property object or a plain value - possibly for a
+    // not-defined property. If we have a plain value, directly return it.
+    if (isset($this->properties[$name])) {
+      return $this->properties[$name]->getValue();
+    }
+    elseif (isset($this->values[$name])) {
+      return $this->values[$name];
     }
   }
 
@@ -183,29 +166,23 @@ public function __set($name, $value) {
    * {@inheritdoc}
    */
   public function __isset($name) {
-    return isset($this->values[$name]) || (isset($this->properties[$name]) && $this->properties[$name]->getValue() !== NULL);
+    if (isset($this->properties[$name])) {
+      return $this->properties[$name]->getValue() !== NULL;
+    }
+    return isset($this->values[$name]);
   }
 
   /**
    * {@inheritdoc}
    */
   public function __unset($name) {
-    $this->set($name, NULL);
-    unset($this->values[$name]);
-  }
-
-  /**
-   * Overrides \Drupal\Core\TypedData\Map::onChange().
-   */
-  public function onChange($property_name) {
-    // Notify the parent of changes.
-    if (isset($this->parent)) {
-      $this->parent->onChange($this->name);
+    if ($this->definition->getPropertyDefinition($name)) {
+      $this->set($name, NULL);
     }
-    // Remove the plain value, such that any further __get() calls go via the
-    // updated property object.
-    if (isset($this->properties[$property_name])) {
-      unset($this->values[$property_name]);
+    else {
+      // Explicitly unset the property in $this->values if a non-defined
+      // property is unset, such that its key is removed from $this->values.
+      unset($this->values[$name]);
     }
   }
 
diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php
index ae21f53292c3d611db7dc8f7d0f051d01fc16ad4..44063ad557a524aea2cf8bab1888a7f99f8a0a1e 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php
@@ -141,13 +141,9 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
    */
   public function setValue($values, $notify = TRUE) {
     if (isset($values) && !is_array($values)) {
-      // Directly update the property instead of invoking the parent, so it can
-      // handle objects and IDs.
-      $this->properties['entity']->setValue($values, $notify);
-      // If notify was FALSE, ensure the target_id property gets synched.
-      if (!$notify) {
-        $this->set('target_id', $this->properties['entity']->getTargetIdentifier(), FALSE);
-      }
+      // If either a scalar or an object was passed as the value for the item,
+      // assign it to the 'entity' property since that works for both cases.
+      $this->set('entity', $values, $notify);
     }
     else {
       // Make sure that the 'entity' property gets set as 'target_id'.
@@ -175,15 +171,15 @@ public function getValue() {
   /**
    * {@inheritdoc}
    */
-  public function onChange($property_name) {
+  public function onChange($property_name, $notify = TRUE) {
     // Make sure that the target ID and the target property stay in sync.
     if ($property_name == 'target_id') {
-      $this->properties['entity']->setValue($this->target_id, FALSE);
+      $this->writePropertyValue('entity', $this->target_id);
     }
     elseif ($property_name == 'entity') {
-      $this->set('target_id', $this->properties['entity']->getTargetIdentifier(), FALSE);
+      $this->writePropertyValue('target_id', $this->get('entity')->getTargetIdentifier());
     }
-    parent::onChange($property_name);
+    parent::onChange($property_name, $notify);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/LanguageItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/LanguageItem.php
index 961eb67d8ed9dba145c32e0b45f507d8e81fe9ee..851b818cdc0e1b7dce4972f7a45ca9888a8bd3ee 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/LanguageItem.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/LanguageItem.php
@@ -69,14 +69,7 @@ public function setValue($values, $notify = TRUE) {
     // Treat the values as property value of the language property, if no array
     // is given as this handles language codes and objects.
     if (isset($values) && !is_array($values)) {
-      // Directly update the property instead of invoking the parent, so that
-      // the language property can take care of updating the language code
-      // property.
-      $this->properties['language']->setValue($values, $notify);
-      // If notify was FALSE, ensure the value property gets synched.
-      if (!$notify) {
-        $this->set('value', $this->properties['language']->getTargetIdentifier(), FALSE);
-      }
+      $this->set('language', $values, $notify);
     }
     else {
       // Make sure that the 'language' property gets set as 'value'.
@@ -100,14 +93,15 @@ public function applyDefaultValue($notify = TRUE) {
   /**
    * {@inheritdoc}
    */
-  public function onChange($property_name) {
+  public function onChange($property_name, $notify = TRUE) {
     // Make sure that the value and the language property stay in sync.
     if ($property_name == 'value') {
-      $this->properties['language']->setValue($this->value, FALSE);
+      $this->writePropertyValue('language', $this->value);
     }
     elseif ($property_name == 'language') {
-      $this->set('value', $this->properties['language']->getTargetIdentifier(), FALSE);
+      $this->writePropertyValue('value', $this->get('language')->getTargetIdentifier());
     }
-    parent::onChange($property_name);
+    parent::onChange($property_name, $notify);
   }
+
 }
diff --git a/core/lib/Drupal/Core/TypedData/ComplexDataInterface.php b/core/lib/Drupal/Core/TypedData/ComplexDataInterface.php
index e7b91fc26fce27fe652f391d5e74aed5571119a7..b60f84d0351e7fca1c0c4d3bb404229c379d0f88 100644
--- a/core/lib/Drupal/Core/TypedData/ComplexDataInterface.php
+++ b/core/lib/Drupal/Core/TypedData/ComplexDataInterface.php
@@ -53,8 +53,7 @@ public function get($property_name);
    *   TRUE. If the update stems from a parent object, set it to FALSE to avoid
    *   being notified again.
    *
-   * @return \Drupal\Core\TypedData\TypedDataInterface
-   *   The property object.
+   * @return $this
    *
    * @throws \InvalidArgumentException
    *   If the specified property does not exist.
diff --git a/core/lib/Drupal/Core/TypedData/Plugin/DataType/Map.php b/core/lib/Drupal/Core/TypedData/Plugin/DataType/Map.php
index 56bdfa26bc4e1b064226f3bd37cf8725cc41694b..7694db550ed1e677e46f0555263eb7f411e28ac7 100644
--- a/core/lib/Drupal/Core/TypedData/Plugin/DataType/Map.php
+++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/Map.php
@@ -94,11 +94,11 @@ public function setValue($values, $notify = TRUE) {
 
     // Update any existing property objects.
     foreach ($this->properties as $name => $property) {
-      $value = NULL;
-      if (isset($values[$name])) {
-        $value = $values[$name];
-      }
+      $value = isset($values[$name]) ? $values[$name] : NULL;
       $property->setValue($value, FALSE);
+      // Remove the value from $this->values to ensure it does not contain any
+      // value for computed properties.
+      unset($this->values[$name]);
     }
     // Notify the parent of any changes.
     if ($notify && isset($this->parent)) {
@@ -134,19 +134,34 @@ public function get($property_name) {
   }
 
   /**
-   * Implements \Drupal\Core\TypedData\ComplexDataInterface::set().
+   * {@inheritdoc}
    */
   public function set($property_name, $value, $notify = TRUE) {
+    // Separate the writing in a protected method, such that onChange
+    // implementations can make use of it.
+    $this->writePropertyValue($property_name, $value);
+    $this->onChange($property_name, $notify);
+    return $this;
+  }
+
+  /**
+   * Writes the value of a property without handling changes.
+   *
+   * Implementations of onChange() should use this method instead of set() in
+   * order to avoid onChange() being triggered again.
+   *
+   * @param string $property_name
+   *   The name of the property to be written.
+   * @param $value
+   *   The value to set.
+   */
+  protected function writePropertyValue($property_name, $value) {
     if ($this->definition->getPropertyDefinition($property_name)) {
-      $this->get($property_name)->setValue($value, $notify);
+      $this->get($property_name)->setValue($value, FALSE);
     }
     else {
       // Just set the plain value, which allows adding a new entry to the map.
       $this->values[$property_name] = $value;
-      // Directly notify ourselves.
-      if ($notify) {
-        $this->onChange($property_name, $value);
-      }
     }
   }
 
@@ -212,11 +227,16 @@ public function __clone() {
   }
 
   /**
-   * Implements \Drupal\Core\TypedData\ComplexDataInterface::onChange().
+   * {@inheritdoc}
+   *
+   * @param bool $notify
+   *   (optional) Whether to forward the notification to the parent. Defaults to
+   *   TRUE. By passing FALSE, overrides of this method can re-use the logic
+   *   of parent classes without triggering notification.
    */
-  public function onChange($property_name) {
+  public function onChange($property_name, $notify = TRUE) {
     // Notify the parent of changes.
-    if (isset($this->parent)) {
+    if ($notify && isset($this->parent)) {
       $this->parent->onChange($this->name);
     }
   }
diff --git a/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php b/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php
index f63ffd62a05b9496dce189bbe22f8524b3fee7c6..6ecf3194124070a5be265352c8f87ff4092522d7 100644
--- a/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php
+++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php
@@ -131,13 +131,12 @@ public function isEmpty() {
   /**
    * {@inheritdoc}
    */
-  public function onChange($property_name) {
-    parent::onChange($property_name);
-
+  public function onChange($property_name, $notify = TRUE) {
     // Enforce that the computed date is recalculated.
     if ($property_name == 'value') {
       $this->date = NULL;
     }
+    parent::onChange($property_name, $notify);
   }
 
 }
diff --git a/core/modules/system/src/Tests/Entity/EntityFieldTest.php b/core/modules/system/src/Tests/Entity/EntityFieldTest.php
index 41dbb44fbac68f9f08df5274e4a35dfd1e5ae62c..45709b9b8a5069511e7bb16098f067c04bc3fc20 100644
--- a/core/modules/system/src/Tests/Entity/EntityFieldTest.php
+++ b/core/modules/system/src/Tests/Entity/EntityFieldTest.php
@@ -130,16 +130,16 @@ protected function doTestReadWrite($entity_type) {
     $this->assertEqual($this->entity_user->getUsername(), $entity->user_id->entity->name->value, format_string('%entity_type: User name can be read.', array('%entity_type' => $entity_type)));
 
     // Change the assigned user by entity.
-    $new_user = $this->createUser();
-    $entity->user_id->entity = $new_user;
-    $this->assertEqual($new_user->id(), $entity->user_id->target_id, format_string('%entity_type: Updated user id can be read.', array('%entity_type' => $entity_type)));
-    $this->assertEqual($new_user->getUsername(), $entity->user_id->entity->name->value, format_string('%entity_type: Updated username value can be read.', array('%entity_type' => $entity_type)));
+    $new_user1 = $this->createUser();
+    $entity->user_id->entity = $new_user1;
+    $this->assertEqual($new_user1->id(), $entity->user_id->target_id, format_string('%entity_type: Updated user id can be read.', array('%entity_type' => $entity_type)));
+    $this->assertEqual($new_user1->getUsername(), $entity->user_id->entity->name->value, format_string('%entity_type: Updated username value can be read.', array('%entity_type' => $entity_type)));
 
     // Change the assigned user by id.
-    $new_user = $this->createUser();
-    $entity->user_id->target_id = $new_user->id();
-    $this->assertEqual($new_user->id(), $entity->user_id->target_id, format_string('%entity_type: Updated user id can be read.', array('%entity_type' => $entity_type)));
-    $this->assertEqual($new_user->getUsername(), $entity->user_id->entity->name->value, format_string('%entity_type: Updated username value can be read.', array('%entity_type' => $entity_type)));
+    $new_user2 = $this->createUser();
+    $entity->user_id->target_id = $new_user2->id();
+    $this->assertEqual($new_user2->id(), $entity->user_id->target_id, format_string('%entity_type: Updated user id can be read.', array('%entity_type' => $entity_type)));
+    $this->assertEqual($new_user2->getUsername(), $entity->user_id->entity->name->value, format_string('%entity_type: Updated username value can be read.', array('%entity_type' => $entity_type)));
 
     // Try unsetting a field.
     $entity->name->value = NULL;
@@ -148,6 +148,34 @@ protected function doTestReadWrite($entity_type) {
     $this->assertNull($entity->user_id->target_id, format_string('%entity_type: User ID field is not set.', array('%entity_type' => $entity_type)));
     $this->assertNull($entity->user_id->entity, format_string('%entity_type: User entity field is not set.', array('%entity_type' => $entity_type)));
 
+    // Test setting the values via the typed data API works as well.
+    // Change the assigned user by entity.
+    $entity->user_id->first()->get('entity')->setValue($new_user2);
+    $this->assertEqual($new_user2->id(), $entity->user_id->target_id, format_string('%entity_type: Updated user id can be read.', array('%entity_type' => $entity_type)));
+    $this->assertEqual($new_user2->getUsername(), $entity->user_id->entity->name->value, format_string('%entity_type: Updated user name value can be read.', array('%entity_type' => $entity_type)));
+
+     // Change the assigned user by id.
+    $entity->user_id->first()->get('target_id')->setValue($new_user2->id());
+    $this->assertEqual($new_user2->id(), $entity->user_id->target_id, format_string('%entity_type: Updated user id can be read.', array('%entity_type' => $entity_type)));
+    $this->assertEqual($new_user2->getUsername(), $entity->user_id->entity->name->value, format_string('%entity_type: Updated user name value can be read.', array('%entity_type' => $entity_type)));
+
+    // Try unsetting a field.
+    $entity->name->first()->get('value')->setValue(NULL);
+    $entity->user_id->first()->get('target_id')->setValue(NULL);
+    $this->assertNull($entity->name->value, format_string('%entity_type: Name field is not set.', array('%entity_type' => $entity_type)));
+    $this->assertNull($entity->user_id->target_id, format_string('%entity_type: User ID field is not set.', array('%entity_type' => $entity_type)));
+    $this->assertNull($entity->user_id->entity, format_string('%entity_type: User entity field is not set.', array('%entity_type' => $entity_type)));
+
+    // Create a fresh entity so target_id does not get its property object
+    // instantiated, then verify setting a new value via typed data API works.
+    $entity2 = entity_create($entity_type, array(
+      'user_id' => array('target_id' => $new_user1->id()),
+    ));
+    // Access the property object, and set a value.
+    $entity2->user_id->first()->get('target_id')->setValue($new_user2->id());
+    $this->assertEqual($new_user2->id(), $entity2->user_id->target_id, format_string('%entity_type: Updated user id can be read.', array('%entity_type' => $entity_type)));
+    $this->assertEqual($new_user2->name->value, $entity2->user_id->entity->name->value, format_string('%entity_type: Updated user name value can be read.', array('%entity_type' => $entity_type)));
+
     // Test using isset(), empty() and unset().
     $entity->name->value = 'test unset';
     unset($entity->name->value);
diff --git a/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php
index e08f9513734beea9cf8eb08f81e629525a56b47a..972b6e274e30a1c621961c7e188a6538b7280b38 100644
--- a/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php
+++ b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php
@@ -59,20 +59,16 @@ public function isEmpty() {
   /**
    * {@inheritdoc}
    */
-  public function onChange($property_name) {
-    // Notify the parent of changes.
-    if (isset($this->parent)) {
-      $this->parent->onChange($this->name);
-    }
-
+  public function onChange($property_name, $notify = TRUE) {
     // Unset processed properties that are affected by the change.
     foreach ($this->definition->getPropertyDefinitions() as $property => $definition) {
       if ($definition->getClass() == '\Drupal\text\TextProcessed') {
         if ($property_name == 'format' || ($definition->getSetting('text source') == $property_name)) {
-          $this->set($property, NULL, FALSE);
+          $this->writePropertyValue($property, NULL);
         }
       }
     }
+    parent::onChange($property_name, $notify);
   }
 
   /**