diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index f49b521..330ccaf 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -806,12 +806,9 @@ function install_tasks_to_display($install_state) {
 function install_get_form($form_id, array &$install_state) {
   // Ensure the form will not redirect, since install_run_tasks() uses a custom
   // redirection logic.
-  $form_state = new FormState(array(
-    'build_info' => array(
-      'args' => array(&$install_state),
-    ),
-    'no_redirect' => TRUE,
-  ));
+  $form_state = (new FormState())
+    ->addBuildInfo('args', [&$install_state])
+    ->disableRedirect();
   $form_builder = \Drupal::formBuilder();
   if ($install_state['interactive']) {
     $form = $form_builder->buildForm($form_id, $form_state);
@@ -827,7 +824,7 @@ function install_get_form($form_id, array &$install_state) {
     // values taken from the installation state.
     $install_form_id = $form_builder->getFormId($form_id, $form_state);
     if (!empty($install_state['forms'][$install_form_id])) {
-      $form_state->set('values', $install_state['forms'][$install_form_id]);
+      $form_state->setValues($install_state['forms'][$install_form_id]);
     }
     $form_builder->submitForm($form_id, $form_state);
 
diff --git a/core/lib/Drupal/Core/Block/BlockBase.php b/core/lib/Drupal/Core/Block/BlockBase.php
index ed2a63a..88d3a46 100644
--- a/core/lib/Drupal/Core/Block/BlockBase.php
+++ b/core/lib/Drupal/Core/Block/BlockBase.php
@@ -356,9 +356,7 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
 
     foreach ($this->getVisibilityConditions() as $condition_id => $condition) {
       // Allow the condition to validate the form.
-      $condition_values = new FormState(array(
-        'values' => $form_state->getValue(array('visibility', $condition_id)),
-      ));
+      $condition_values = (new FormState())->setValues($form_state->getValue(array('visibility', $condition_id)));
       $condition->validateConfigurationForm($form, $condition_values);
       // Update the original form values.
       $form_state->setValue(array('visibility', $condition_id), $condition_values['values']);
@@ -389,9 +387,7 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s
       $this->configuration['cache'] = $form_state->getValue('cache');
       foreach ($this->getVisibilityConditions() as $condition_id => $condition) {
         // Allow the condition to submit the form.
-        $condition_values = new FormState(array(
-          'values' => $form_state->getValue(array('visibility', $condition_id)),
-        ));
+        $condition_values = (new FormState())->setValues($form_state->getValue(array('visibility', $condition_id)));
         $condition->submitConfigurationForm($form, $condition_values);
         // Update the original form values.
         $form_state->setValue(array('visibility', $condition_id), $condition_values['values']);
diff --git a/core/lib/Drupal/Core/Entity/EntityFormBuilder.php b/core/lib/Drupal/Core/Entity/EntityFormBuilder.php
index 3a825be..2c9fb68 100644
--- a/core/lib/Drupal/Core/Entity/EntityFormBuilder.php
+++ b/core/lib/Drupal/Core/Entity/EntityFormBuilder.php
@@ -49,7 +49,7 @@ public function getForm(EntityInterface $entity, $operation = 'default', array $
     $form_object = $this->entityManager->getFormObject($entity->getEntityTypeId(), $operation);
     $form_object->setEntity($entity);
 
-    $form_state = new FormState($form_state_additions);
+    $form_state = (new FormState())->setFormState($form_state_additions);
     $form_state['build_info']['callback_object'] = $form_object;
     $form_state['build_info']['base_form_id'] = $form_object->getBaseFormID();
     $form_state['build_info'] += array('args' => array());
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index b1e2260..0a9fe92 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -240,7 +240,9 @@ public function buildForm($form_id, FormStateInterface &$form_state) {
       // - temporary: Any assigned data is expected to survives within the same
       //   page request.
       if ($check_cache) {
-        $cache_form_state = $form_state->getCacheableArray(array('always_process', 'temporary'));
+        $cache_form_state = $form_state->getCacheableArray();
+        $cache_form_state['always_process'] = $form_state->getAlwaysProcess();
+        $cache_form_state['temporary'] = $form_state->getTemporary();
         $form_state = $form_state_before_retrieval;
         $form_state->setFormState($cache_form_state);
       }
@@ -281,6 +283,8 @@ public function buildForm($form_id, FormStateInterface &$form_state) {
    */
   public function rebuildForm($form_id, FormStateInterface &$form_state, $old_form = NULL) {
     $form = $this->retrieveForm($form_id, $form_state);
+    // All rebuilt forms will be cached.
+    $form_state->setCached();
 
     // If only parts of the form will be returned to the browser (e.g., Ajax or
     // RIA clients), re-use the old #build_id to not require client-side code to
@@ -309,13 +313,13 @@ public function rebuildForm($form_id, FormStateInterface &$form_state, $old_form
     // cached is the $form structure before it passes through
     // self::doBuildForm(), so we need to do it here.
     // @todo For Drupal 8, find a way to avoid this code duplication.
-    if (empty($form_state['no_cache'])) {
+    if ($form_state->isCached()) {
       $this->setCache($form['#build_id'], $form, $form_state);
     }
 
     // Clear out all group associations as these might be different when
     // re-rendering the form.
-    $form_state->set('groups', array());
+    $form_state->setGroups(array());
 
     // Return a fully built form that is ready for rendering.
     return $this->doBuildForm($form_id, $form, $form_state);
@@ -352,15 +356,15 @@ public function submitForm($form_arg, FormStateInterface &$form_state) {
     // there).
     $form_state->setUserInput($form_state->getValues());
 
-    $form_state->set('programmed', TRUE);
+    $form_state->setProgrammed();
 
     $form_id = $this->getFormId($form_arg, $form_state);
     $form = $this->retrieveForm($form_id, $form_state);
     // Programmed forms are always submitted.
-    $form_state->set('submitted', TRUE);
+    $form_state->setSubmitted();
 
     // Reset form validation.
-    $form_state->set('must_validate', TRUE);
+    $form_state->setValidationEnforced();
     $form_state->clearErrors();
 
     $this->prepareForm($form_id, $form, $form_state);
@@ -411,7 +415,7 @@ public function retrieveForm($form_id, FormStateInterface &$form_state) {
    * {@inheritdoc}
    */
   public function processForm($form_id, &$form, FormStateInterface &$form_state) {
-    $form_state->set('values', array());
+    $form_state->setValues(array());
 
     // With GET, these forms are always submitted if requested.
     if ($form_state['method'] == 'get' && !empty($form_state['always_process'])) {
@@ -449,7 +453,7 @@ public function processForm($form_id, &$form, FormStateInterface &$form_state) {
       // exactly one submit button in the form, and if so, automatically use it
       // as triggering_element.
       if ($form_state['programmed'] && !isset($form_state['triggering_element']) && count($form_state['buttons']) == 1) {
-        $form_state->set('triggering_element', reset($form_state['buttons']));
+        $form_state->setTriggeringElement(reset($form_state['buttons']));
       }
       $this->formValidator->validateForm($form_id, $form, $form_state);
 
@@ -505,7 +509,7 @@ public function processForm($form_id, &$form, FormStateInterface &$form_state) {
     // self::doBuildForm(), because self::doBuildForm() must run for each
     // request to accommodate new user input. Rebuilt forms are not cached here,
     // because self::rebuildForm() already takes care of that.
-    if (!$form_state['rebuild'] && $form_state['cache'] && empty($form_state['no_cache'])) {
+    if (!$form_state->getRebuild() && $form_state->isCached()) {
       $this->setCache($form['#build_id'], $unprocessed_form, $form_state);
     }
   }
@@ -517,10 +521,9 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
     $user = $this->currentUser();
 
     $form['#type'] = 'form';
-    $form_state->set('programmed', isset($form_state['programmed']) ? $form_state['programmed'] : FALSE);
 
     // Fix the form method, if it is 'get' in $form_state, but not in $form.
-    if ($form_state->get('method') == 'get' && !isset($form['#method'])) {
+    if ($form_state->getMethod() == 'get' && !isset($form['#method'])) {
       $form['#method'] = 'get';
     }
 
@@ -692,10 +695,10 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state
       // coming from the POST data is set and matches the current form_id.
       $input = $form_state->getUserInput();
       if ($form_state['programmed'] || (!empty($input) && (isset($input['form_id']) && ($input['form_id'] == $form_id)))) {
-        $form_state->set('process_input', TRUE);
+        $form_state->setProcessInput();
       }
       else {
-        $form_state->set('process_input', FALSE);
+        $form_state->setProcessInput(FALSE);
       }
 
       // All form elements should have an #array_parents property.
@@ -792,7 +795,7 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state
     // If there is a file element, we need to flip a flag so later the
     // form encoding can be set.
     if (isset($element['#type']) && $element['#type'] == 'file') {
-      $form_state->set('has_file_element', TRUE);
+      $form_state->setHasFileElement();
     }
 
     // Final tasks for the form element after self::doBuildForm() has run for
@@ -810,23 +813,24 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state
       // consistent as we can be across browsers, if no 'triggering_element' has
       // been identified yet, default it to the first button.
       if (!$form_state['programmed'] && !isset($form_state['triggering_element']) && !empty($form_state['buttons'])) {
-        $form_state->set('triggering_element', $form_state['buttons'][0]);
+        $form_state->setTriggeringElement($form_state['buttons'][0]);
       }
 
-      $triggering_element = $form_state->get('triggering_element');
+      $triggering_element = $form_state->getTriggeringElement();
       // If the triggering element specifies "button-level" validation and
       // submit handlers to run instead of the default form-level ones, then add
       // those to the form state.
-      foreach (array('validate', 'submit') as $type) {
-        if (isset($triggering_element['#' . $type])) {
-          $form_state->set($type . '_handlers', $triggering_element['#' . $type]);
-        }
+      if (isset($triggering_element['#validate'])) {
+        $form_state->setValidateHandlers($triggering_element['#validate']);
+      }
+      if (isset($triggering_element['#submit'])) {
+        $form_state->setSubmitHandlers($triggering_element['#submit']);
       }
 
       // If the triggering element executes submit handlers, then set the form
       // state key that's needed for those handlers to run.
       if (!empty($triggering_element['#executes_submit_callback'])) {
-        $form_state->set('submitted', TRUE);
+        $form_state->setSubmitted();
       }
 
       // Special processing if the triggering element is a button.
@@ -968,7 +972,7 @@ protected function handleInputElement($form_id, &$element, FormStateInterface &$
     if ($process_input) {
       // Detect if the element triggered the submission via Ajax.
       if ($this->elementTriggeredScriptedSubmission($element, $form_state)) {
-        $form_state->set('triggering_element', $element);
+        $form_state->setTriggeringElement($element);
       }
 
       // If the form was submitted by the browser rather than via Ajax, then it
@@ -980,11 +984,11 @@ protected function handleInputElement($form_id, &$element, FormStateInterface &$
         // form_state_values_clean() and for the self::doBuildForm() code that
         // handles a form submission containing no button information in
         // \Drupal::request()->request.
-        $buttons = $form_state->get('buttons');
+        $buttons = $form_state->getButtons();
         $buttons[] = $element;
-        $form_state->set('buttons', $buttons);
+        $form_state->setButtons($buttons);
         if ($this->buttonWasClicked($element, $form_state)) {
-          $form_state->set('triggering_element', $element);
+          $form_state->setTriggeringElement($element);
         }
       }
     }
diff --git a/core/lib/Drupal/Core/Form/FormBuilderInterface.php b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
index 3f16f50..b62a0c5 100644
--- a/core/lib/Drupal/Core/Form/FormBuilderInterface.php
+++ b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
@@ -148,7 +148,7 @@ public function rebuildForm($form_id, FormStateInterface &$form_state, $old_form
    *   @endcode
    *   would be called via self::submitForm() as follows:
    *   @code
-   *   $form_state->set('values', $my_form_values);
+   *   $form_state->setValues($my_form_values);
    *   $form_state['build_info']['args'] = array(&$object);
    *   drupal_form_submit('mymodule_form', $form_state);
    *   @endcode
@@ -161,7 +161,7 @@ public function rebuildForm($form_id, FormStateInterface &$form_state, $old_form
    * $values['pass']['pass1'] = 'password';
    * $values['pass']['pass2'] = 'password';
    * $values['op'] = t('Create new account');
-   * $form_state->set('values', $values);
+   * $form_state->setValues($values);
    * drupal_form_submit('user_register_form', $form_state);
    * @endcode
    */
diff --git a/core/lib/Drupal/Core/Form/FormState.php b/core/lib/Drupal/Core/Form/FormState.php
index 5bed64a..7b12947 100644
--- a/core/lib/Drupal/Core/Form/FormState.php
+++ b/core/lib/Drupal/Core/Form/FormState.php
@@ -26,13 +26,6 @@ class FormState implements FormStateInterface, \ArrayAccess {
   protected static $anyErrors = FALSE;
 
   /**
-   * The internal storage of the form state.
-   *
-   * @var array
-   */
-  protected $internalStorage = array();
-
-  /**
    * The complete form structure.
    *
    * #process, #after_build, #element_validate, and other handlers being invoked
@@ -217,10 +210,6 @@ class FormState implements FormStateInterface, \ArrayAccess {
   /**
    * If TRUE and the method is GET, a form_id is not necessary.
    *
-   * This should only be used on RESTful GET forms that do NOT write data, as
-   * this could lead to security issues. It is useful so that searches do not
-   * need to have a form_id in their query arguments to trigger the search.
-   *
    * This property is uncacheable.
    *
    * @var bool
@@ -408,35 +397,381 @@ class FormState implements FormStateInterface, \ArrayAccess {
   protected $submit_handlers;
 
   /**
-   * Constructs a \Drupal\Core\Form\FormState object.
-   *
-   * @param array $form_state_additions
-   *   (optional) An associative array used to build the current state of the
-   *   form. Use this to pass additional information to the form, such as the
-   *   langcode. Defaults to an empty array.
+   * {@inheritdoc}
    */
-  public function __construct(array $form_state_additions = array()) {
-    $this->setFormState($form_state_additions);
+  public function setFormState(array $form_state_additions) {
+    foreach ($form_state_additions as $key => $value) {
+      if (property_exists($this, $key)) {
+        $this->{$key} = $value;
+      }
+      else {
+        $this->set($key, $value);
+      }
+    }
+    return $this;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function setFormState(array $form_state_additions) {
-    foreach ($form_state_additions as $key => $value) {
-      $this->set($key, $value);
-    }
+  public function setAlwaysProcess($always_process = TRUE) {
+    $this->always_process = (bool) $always_process;
     return $this;
   }
 
   /**
    * {@inheritdoc}
    */
+  public function getAlwaysProcess() {
+    return $this->always_process;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setBuildInfo(array $build_info) {
+    $this->build_info = $build_info;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBuildInfo() {
+    return $this->build_info;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setButtons(array $buttons) {
+    $this->buttons = $buttons;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getButtons() {
+    return $this->buttons;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCached($cache = TRUE) {
+    $this->cache = (bool) $cache;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCached() {
+    return empty($this->no_cache) && $this->cache;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function disableCache() {
+    $this->no_cache = TRUE;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setExecuted() {
+    $this->executed = TRUE;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isExecuted() {
+    return $this->executed;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setGroups(array $groups) {
+    $this->groups = $groups;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getGroups() {
+    return $this->groups;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setHasFileElement($has_file_element = TRUE) {
+    $this->has_file_element = (bool) $has_file_element;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasFileElement() {
+    return $this->has_file_element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setInput($input) {
+    $this->input = $input;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInput() {
+    return $this->input;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setLimitValidationErrors($limit_validation_errors) {
+    $this->limit_validation_errors = $limit_validation_errors;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLimitValidationErrors() {
+    return $this->limit_validation_errors;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setMethod($method) {
+    $this->method = $method;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMethod() {
+    return $this->method;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValidationEnforced($must_validate = TRUE) {
+    $this->must_validate = (bool) $must_validate;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isValidationEnforced() {
+    return $this->must_validate;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function disableRedirect($no_redirect = TRUE) {
+    $this->no_redirect = (bool) $no_redirect;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isRedirectDisabled() {
+   return $this->no_redirect;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setProcessInput($process_input = TRUE) {
+    $this->process_input = (bool) $process_input;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isProcessingInput() {
+    return $this->process_input;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setProgrammed($programmed = TRUE) {
+    $this->programmed = (bool) $programmed;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isProgrammed() {
+    return $this->programmed;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setProgrammedBypassAccessCheck($programmed_bypass_access_check = TRUE) {
+    $this->programmed_bypass_access_check = (bool) $programmed_bypass_access_check;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isBypassingProgrammedAccessChecks() {
+    return $this->programmed_bypass_access_check;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRebuildInfo(array $rebuild_info) {
+    $this->rebuild_info = $rebuild_info;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRebuildInfo() {
+    return $this->rebuild_info;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setStorage($storage) {
+    $this->storage = $storage;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getStorage() {
+    return $this->storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setSubmitHandlers($submit_handlers) {
+    $this->submit_handlers = $submit_handlers;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSubmitHandlers() {
+    return $this->submit_handlers;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setSubmitted() {
+    $this->submitted = TRUE;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isSubmitted() {
+    return $this->submitted;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTemporary(array $temporary) {
+    $this->temporary = $temporary;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTemporary() {
+    return $this->temporary;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTriggeringElement($triggering_element) {
+    $this->triggering_element = $triggering_element;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTriggeringElement() {
+    return $this->triggering_element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValidateHandlers($validate_handlers) {
+    $this->validate_handlers = $validate_handlers;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValidateHandlers() {
+    return $this->validate_handlers;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValidationComplete($validation_complete = TRUE) {
+    $this->validation_complete = (bool) $validation_complete;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isValidationComplete() {
+    return $this->validation_complete;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function loadInclude($module, $type, $name = NULL) {
     if (!isset($name)) {
       $name = $module;
     }
-    $build_info = $this->get('build_info');
+    $build_info = $this->getBuildInfo();
     if (!isset($build_info['files']["$module:$name.$type"])) {
       // Only add successfully included files to the form state.
       if ($result = $this->moduleLoadInclude($module, $type, $name)) {
@@ -445,7 +780,7 @@ public function loadInclude($module, $type, $name = NULL) {
           'module' => $module,
           'name' => $name,
         );
-        $this->set('build_info', $build_info);
+        $this->setBuildInfo($build_info);
         return $result;
       }
     }
@@ -455,8 +790,8 @@ public function loadInclude($module, $type, $name = NULL) {
   /**
    * {@inheritdoc}
    */
-  public function getCacheableArray($allowed_keys = array()) {
-    $cacheable_array = array(
+  public function getCacheableArray() {
+    return array(
       'build_info' => $this->build_info,
       'response' => $this->response,
       'cache' => $this->cache,
@@ -466,11 +801,7 @@ public function getCacheableArray($allowed_keys = array()) {
       'process_input' => $this->process_input,
       'has_file_element' => $this->has_file_element,
       'storage' => $this->storage,
-    ) + $this->internalStorage;
-    foreach ($allowed_keys as $allowed_key) {
-      $cacheable_array[$allowed_key] = $this->get($allowed_key);
-    }
-    return $cacheable_array;
+    );
   }
 
   /**
@@ -494,7 +825,7 @@ public function &getCompleteForm() {
    * @deprecated in Drupal 8.0.x, might be removed before Drupal 8.0.0.
    */
   public function offsetExists($offset) {
-    return isset($this->{$offset}) || isset($this->internalStorage[$offset]);
+    return isset($this->{$offset}) || isset($this->storage[$offset]);
   }
 
   /**
@@ -503,7 +834,15 @@ public function offsetExists($offset) {
    * @deprecated in Drupal 8.0.x, might be removed before Drupal 8.0.0.
    */
   public function &offsetGet($offset) {
-    $value = &$this->get($offset);
+    if (property_exists($this, $offset)) {
+      $value = &$this->{$offset};
+    }
+    else {
+      if (!isset($this->storage[$offset])) {
+        $this->storage[$offset] = NULL;
+      }
+      $value = &$this->get($offset);
+    }
     return $value;
   }
 
@@ -513,7 +852,12 @@ public function &offsetGet($offset) {
    * @deprecated in Drupal 8.0.x, might be removed before Drupal 8.0.0.
    */
   public function offsetSet($offset, $value) {
-    $this->set($offset, $value);
+    if (property_exists($this, $offset)) {
+      $this->{$offset} = $value;
+    }
+    else {
+      $this->set($offset, $value);
+    }
   }
 
   /**
@@ -525,44 +869,24 @@ public function offsetUnset($offset) {
     if (property_exists($this, $offset)) {
       $this->{$offset} = NULL;
     }
-    unset($this->internalStorage[$offset]);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setIfNotExists($property, $value) {
-    if (!$this->has($property)) {
-      $this->set($property, $value);
+    else {
+      unset($this->storage[$offset]);
     }
-    return $this;
   }
 
   /**
    * {@inheritdoc}
    */
   public function &get($property) {
-    if (property_exists($this, $property)) {
-      return $this->{$property};
-    }
-    else {
-      if (!isset($this->internalStorage[$property])) {
-        $this->internalStorage[$property] = NULL;
-      }
-      return $this->internalStorage[$property];
-    }
+    $value = &NestedArray::getValue($this->storage, (array) $property);
+    return $value;
   }
 
   /**
    * {@inheritdoc}
    */
   public function set($property, $value) {
-    if (property_exists($this, $property)) {
-      $this->{$property} = $value;
-    }
-    else {
-      $this->internalStorage[$property] = $value;
-    }
+    NestedArray::setValue($this->storage, (array) $property, $value, TRUE);
     return $this;
   }
 
@@ -570,20 +894,18 @@ public function set($property, $value) {
    * {@inheritdoc}
    */
   public function has($property) {
-    if (property_exists($this, $property)) {
-      return $this->{$property} !== NULL;
-    }
-
-    return array_key_exists($property, $this->internalStorage);
+    $exists = NULL;
+    NestedArray::getValue($this->storage, (array) $property, $exists);
+    return $exists;
   }
 
   /**
    * {@inheritdoc}
    */
   public function addBuildInfo($property, $value) {
-    $build_info = $this->get('build_info');
+    $build_info = $this->getBuildInfo();
     $build_info[$property] = $value;
-    $this->set('build_info', $build_info);
+    $this->setBuildInfo($build_info);
     return $this;
   }
 
@@ -624,6 +946,14 @@ public function &getValue($key, $default = NULL) {
   /**
    * {@inheritdoc}
    */
+  public function setValues(array $values) {
+    $this->values = $values;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function setValue($key, $value) {
     NestedArray::setValue($this->getValues(), (array) $key, $value, TRUE);
     return $this;
@@ -658,7 +988,7 @@ public function isValueEmpty($key) {
   /**
    * {@inheritdoc}
    */
-  public function setValueForElement($element, $value) {
+  public function setValueForElement(array $element, $value) {
     return $this->setValue($element['#parents'], $value);
   }
 
@@ -666,13 +996,20 @@ public function setValueForElement($element, $value) {
    * {@inheritdoc}
    */
   public function setResponse(Response $response) {
-    $this->set('response', $response);
+    $this->response = $response;
     return $this;
   }
 
   /**
    * {@inheritdoc}
    */
+  public function getResponse() {
+    return $this->response;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function setRedirect($route_name, array $route_parameters = array(), array $options = array()) {
     $url = new Url($route_name, $route_parameters, $options);
     return $this->setRedirectUrl($url);
@@ -682,7 +1019,7 @@ public function setRedirect($route_name, array $route_parameters = array(), arra
    * {@inheritdoc}
    */
   public function setRedirectUrl(Url $url) {
-    $this->set('redirect', $url);
+    $this->redirect = $url;
     return $this;
   }
 
@@ -692,19 +1029,19 @@ public function setRedirectUrl(Url $url) {
   public function getRedirect() {
     // Skip redirection for form submissions invoked via
     // \Drupal\Core\Form\FormBuilderInterface::submitForm().
-    if ($this->get('programmed')) {
+    if ($this->isProgrammed()) {
       return FALSE;
     }
     // Skip redirection if rebuild is activated.
-    if ($this->get('rebuild')) {
+    if ($this->getRebuild()) {
       return FALSE;
     }
     // Skip redirection if it was explicitly disallowed.
-    if ($this->get('no_redirect')) {
+    if ($this->no_redirect) {
       return FALSE;
     }
 
-    return $this->get('redirect');
+    return $this->redirect;
   }
 
   /**
@@ -728,14 +1065,14 @@ public static function hasAnyErrors() {
    * {@inheritdoc}
    */
   public function setErrorByName($name, $message = '') {
-    if ($this->get('validation_complete')) {
+    if ($this->isValidationComplete()) {
       throw new \LogicException('Form errors cannot be set after form validation has finished.');
     }
 
     $errors = $this->getErrors();
     if (!isset($errors[$name])) {
       $record = TRUE;
-      $limit_validation_errors = $this->get('limit_validation_errors');
+      $limit_validation_errors = $this->getLimitValidationErrors();
       if ($limit_validation_errors !== NULL) {
         // #limit_validation_errors is an array of "sections" within which user
         // input must be valid. If the element is within one of these sections,
@@ -761,7 +1098,7 @@ public function setErrorByName($name, $message = '') {
       }
       if ($record) {
         $errors[$name] = $message;
-        $this->set('errors', $errors);
+        $this->errors = $errors;
         static::setAnyErrors();
         if ($message) {
           $this->drupalSetMessage($message, 'error');
@@ -775,7 +1112,7 @@ public function setErrorByName($name, $message = '') {
   /**
    * {@inheritdoc}
    */
-  public function setError(&$element, $message = '') {
+  public function setError(array &$element, $message = '') {
     $this->setErrorByName(implode('][', $element['#parents']), $message);
     return $this;
   }
@@ -784,14 +1121,14 @@ public function setError(&$element, $message = '') {
    * {@inheritdoc}
    */
   public function clearErrors() {
-    $this->set('errors', array());
+    $this->errors = array();
     static::setAnyErrors(FALSE);
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getError($element) {
+  public function getError(array $element) {
     if ($errors = $this->getErrors($this)) {
       $parents = array();
       foreach ($element['#parents'] as $parent) {
@@ -808,24 +1145,30 @@ public function getError($element) {
    * {@inheritdoc}
    */
   public function getErrors() {
-    return $this->get('errors');
+    return $this->errors;
   }
 
   /**
    * {@inheritdoc}
    */
   public function setRebuild($rebuild = TRUE) {
-    $this->set('rebuild', $rebuild);
+    $this->rebuild = $rebuild;
     return $this;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getRebuild() {
+    return $this->rebuild;
+  }
 
   /**
    * {@inheritdoc}
    */
   public function prepareCallback($callback) {
     if (is_string($callback) && substr($callback, 0, 2) == '::') {
-      $callback = array($this->get('build_info')['callback_object'], substr($callback, 2));
+      $callback = array($this->getFormObject(), substr($callback, 2));
     }
     return $callback;
   }
@@ -834,8 +1177,7 @@ public function prepareCallback($callback) {
    * {@inheritdoc}
    */
   public function getFormObject() {
-    $build_info = $this->get('build_info');
-    return $build_info['callback_object'];
+    return $this->getBuildInfo()['callback_object'];
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Form/FormStateInterface.php b/core/lib/Drupal/Core/Form/FormStateInterface.php
index 5351133..1eaf181 100644
--- a/core/lib/Drupal/Core/Form/FormStateInterface.php
+++ b/core/lib/Drupal/Core/Form/FormStateInterface.php
@@ -96,18 +96,6 @@ public function getCacheableArray();
   public function setFormState(array $form_state_additions);
 
   /**
-   * Sets a value to an arbitrary property if it does not exist yet.
-   *
-   * @param string $property
-   *   The property to use for the value.
-   * @param mixed $value
-   *   The data to store.
-   *
-   * @return $this
-   */
-  public function setIfNotExists($property, $value);
-
-  /**
    * Sets a response for this form.
    *
    * If a response is set, it will be used during processing and returned
@@ -121,6 +109,17 @@ public function setIfNotExists($property, $value);
   public function setResponse(Response $response);
 
   /**
+   * Gets a response for this form.
+   *
+   * If a response is set, it will be used during processing and returned
+   * directly. The form will not be rebuilt or redirected.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response|null
+   *   The response to return, or NULL.
+   */
+  public function getResponse();
+
+  /**
    * Sets the redirect for the form.
    *
    * @param string $route_name
@@ -167,8 +166,11 @@ public function getRedirect();
   /**
    * Gets any arbitrary property.
    *
-   * @param string $property
-   *   The property to retrieve.
+   * @param string|array $property
+   *   Properties are often stored as multi-dimensional associative arrays. If
+   *   $property is a string, it will return $storage[$property]. If $property is
+   *   an array, each element of the array will be used as a nested key. If
+   *   $property = array('foo', 'bar') it will return $storage['foo']['bar'].
    *
    * @return mixed
    *   A reference to the value for that property, or NULL if the property does
@@ -179,8 +181,12 @@ public function &get($property);
   /**
    * Sets a value to an arbitrary property.
    *
-   * @param string $property
-   *   The property to use for the value.
+   * @param string|array $property
+   *   Properties are often stored as multi-dimensional associative arrays. If
+   *   $property is a string, it will use $storage[$property] = $value. If
+   *   $property is an array, each element of the array will be used as a nested
+   *   key. If $property = array('foo', 'bar') it will use
+   *   $storage['foo']['bar'] = $value.
    * @param mixed $value
    *   The value to set.
    *
@@ -189,8 +195,14 @@ public function &get($property);
   public function set($property, $value);
 
   /**
+   * Determines if an arbitrary property is present.
+   *
    * @param string $property
-   *   The property to use for the value.
+   *   Properties are often stored as multi-dimensional associative arrays. If
+   *   $property is a string, it will return isset($storage[$property]). If
+   *   $property is an array, each element of the array will be used as a nested
+   *   key. If $property = array('foo', 'bar') it will return
+   *   isset($storage['foo']['bar']).
    */
   public function has($property);
 
@@ -253,6 +265,19 @@ public function &getValues();
   public function &getValue($key, $default = NULL);
 
   /**
+   * Sets the submitted form values.
+   *
+   * This should be avoided, since these values have been validated already. Use
+   * self::setUserInput() instead.
+   *
+   * @param array $values
+   *   The multi-dimensional associative array of form values.
+   *
+   * @return $this
+   */
+  public function setValues(array $values);
+
+  /**
    * Sets the submitted form value for a specific key.
    *
    * @param string|array $key
@@ -335,7 +360,7 @@ public function isValueEmpty($key);
    *
    * @return $this
    */
-  public function setValueForElement($element, $value);
+  public function setValueForElement(array $element, $value);
 
   /**
    * Determines if any forms have any errors.
@@ -448,7 +473,7 @@ public function setErrorByName($name, $message = '');
    *
    * @return $this
    */
-  public function setError(&$element, $message = '');
+  public function setError(array &$element, $message = '');
 
   /**
    * Clears all errors against all form elements made by self::setErrorByName().
@@ -475,7 +500,7 @@ public function getErrors();
    * @return string|null
    *   Either the error message for this element or NULL if there are no errors.
    */
-  public function getError($element);
+  public function getError(array $element);
 
   /**
    * Sets the form to be rebuilt after processing.
@@ -488,6 +513,14 @@ public function getError($element);
   public function setRebuild($rebuild = TRUE);
 
   /**
+   * Determines if the form should be rebuilt after processing.
+   *
+   * @return bool
+   *   TRUE if the form should be rebuilt, FALSE otherwise.
+   */
+  public function getRebuild();
+
+  /**
    * Converts support notations for a form callback to a valid callable.
    *
    * Specifically, supports methods on the form/callback object as strings when
@@ -509,4 +542,361 @@ public function prepareCallback($callback);
    */
   public function getFormObject();
 
+  /**
+   * Sets this form to always be processed.
+   *
+   * This should only be used on RESTful GET forms that do NOT write data, as
+   * this could lead to security issues. It is useful so that searches do not
+   * need to have a form_id in their query arguments to trigger the search.
+   *
+   * @param bool $always_process
+   *   TRUE if the form should always be processed, FALSE otherwise.
+   *
+   * @return $this
+   */
+  public function setAlwaysProcess($always_process = TRUE);
+
+  /**
+   * Determines if this form should always be processed.
+   *
+   * @return bool
+   *   TRUE if the form should always be processed, FALSE otherwise.
+   */
+  public function getAlwaysProcess();
+
+  /**
+   * Sets the build info for the form.
+   *
+   * @param array $build_info
+   *   An array of build info.
+   *
+   * @return $this
+   *
+   * @see \Drupal\Core\Form\FormState::$build_info
+   */
+  public function setBuildInfo(array $build_info);
+
+  /**
+   * Returns the build info for the form.
+   *
+   * @return array
+   *   An array of build info.
+   *
+   * @see \Drupal\Core\Form\FormState::$build_info
+   */
+  public function getBuildInfo();
+
+  /**
+   * Stores the submit and button elements for the form.
+   *
+   * @param array $buttons
+   *   The submit and button elements.
+   *
+   * @return $this
+   */
+  public function setButtons(array $buttons);
+
+  /**
+   * Returns the submit and button elements for the form.
+   *
+   * @return array
+   *   The submit and button elements.
+   */
+  public function getButtons();
+
+  /**
+   * Sets this form to be cached.
+   *
+   * @param bool $cache
+   *   TRUE if the form should be cached, FALSE otherwise.
+   *
+   * @return $this
+   */
+  public function setCached($cache = TRUE);
+
+  /**
+   * Determines if the form should be cached.
+   *
+   * @return bool
+   *   TRUE if the form should be cached, FALSE otherwise.
+   */
+  public function isCached();
+
+  /**
+   * Prevents the form from being cached.
+   *
+   * @return $this
+   */
+  public function disableCache();
+
+  /**
+   * Sets that the form was submitted and has been processed and executed.
+   *
+   * @return $this
+   */
+  public function setExecuted();
+
+  /**
+   * Determines if the form was submitted and has been processed and executed.
+   *
+   * @return bool
+   *   TRUE if the form was submitted and has been processed and executed.
+   */
+  public function isExecuted();
+
+  /**
+   * Sets references to details elements to render them within vertical tabs.
+   *
+   * @param array $groups
+   *   References to details elements to render them within vertical tabs.
+   *
+   * @return $this
+   */
+  public function setGroups(array $groups);
+
+  /**
+   * Returns references to details elements to render them within vertical tabs.
+   *
+   * @return array
+   */
+  public function getGroups();
+
+  /**
+   * @todo.
+   *
+   * @param bool $has_file_element
+   *
+   * @return $this
+   */
+  public function setHasFileElement($has_file_element = TRUE);
+
+  /**
+   * @todo.
+   *
+   * @return bool
+   */
+  public function hasFileElement();
+
+  /**
+   * @todo.
+   *
+   * @param array|null $limit_validation_errors
+   *
+   * @return $this
+   */
+  public function setLimitValidationErrors($limit_validation_errors);
+
+  /**
+   * @todo.
+   *
+   * @return array|null
+   */
+  public function getLimitValidationErrors();
+
+  /**
+   * @todo.
+   *
+   * @param string $method
+   *
+   * @return $this
+   */
+  public function setMethod($method);
+
+  /**
+   * @todo.
+   *
+   * @return string
+   */
+  public function getMethod();
+
+  /**
+   * @todo.
+   *
+   * @param bool $must_validate
+   *
+   * @return $this
+   */
+  public function setValidationEnforced($must_validate = TRUE);
+
+  /**
+   * @todo.
+   *
+   * @return bool
+   */
+  public function isValidationEnforced();
+
+  /**
+   * @todo.
+   *
+   * @param bool $no_redirect
+   *
+   * @return $this
+   */
+  public function disableRedirect($no_redirect = TRUE);
+
+  /**
+   * @todo.
+   *
+   * @return bool
+   */
+  public function isRedirectDisabled();
+
+  /**
+   * @todo.
+   *
+   * @param bool $process_input
+   *
+   * @return $this
+   */
+  public function setProcessInput($process_input = TRUE);
+
+  /**
+   * @todo.
+   *
+   * @return bool
+   */
+  public function isProcessingInput();
+
+  /**
+   * @todo.
+   *
+   * @param bool $programmed
+   *
+   * @return $this
+   */
+  public function setProgrammed($programmed = TRUE);
+
+  /**
+   * @todo.
+   *
+   * @return bool
+   */
+  public function isProgrammed();
+
+  /**
+   * @todo.
+   *
+   * @param bool $programmed_bypass_access_check
+   *
+   * @return $this
+   */
+  public function setProgrammedBypassAccessCheck($programmed_bypass_access_check = TRUE);
+
+  /**
+   * @todo.
+   *
+   * @return bool
+   */
+  public function isBypassingProgrammedAccessChecks();
+
+  /**
+   * @todo.
+   *
+   * @param array $rebuild_info
+   *
+   * @return $this
+   */
+  public function setRebuildInfo(array $rebuild_info);
+
+  /**
+   * @todo.
+   *
+   * @return array
+   */
+  public function getRebuildInfo();
+
+  /**
+   * @todo.
+   *
+   * @param array|null $submit_handlers
+   *
+   * @return $this
+   */
+  public function setSubmitHandlers($submit_handlers);
+
+  /**
+   * @todo.
+   *
+   * @return array|null
+   */
+  public function getSubmitHandlers();
+
+  /**
+   * @todo.
+   *
+   * @return $this
+   */
+  public function setSubmitted();
+
+  /**
+   * @todo.
+   *
+   * @return bool
+   */
+  public function isSubmitted();
+
+  /**
+   * @todo.
+   *
+   * @param array $temporary
+   *
+   * @return $this
+   */
+  public function setTemporary(array $temporary);
+
+  /**
+   * @todo.
+   *
+   * @return array
+   */
+  public function getTemporary();
+
+  /**
+   * @todo.
+   *
+   * @param array|null $triggering_element
+   *
+   * @return $this
+   */
+  public function setTriggeringElement($triggering_element);
+
+  /**
+   * @todo.
+   *
+   * @return array|null
+   */
+  public function getTriggeringElement();
+
+  /**
+   * @todo.
+   *
+   * @param array|null $validate_handlers
+   *
+   * @return $this
+   */
+  public function setValidateHandlers($validate_handlers);
+
+  /**
+   * @todo.
+   *
+   * @return array|null
+   */
+  public function getValidateHandlers();
+
+  /**
+   * @todo.
+   *
+   * @param bool $validation_complete
+   *
+   * @return $this
+   */
+  public function setValidationComplete($validation_complete = TRUE);
+
+  /**
+   * @todo.
+   *
+   * @return bool
+   */
+  public function isValidationComplete();
+
 }
diff --git a/core/lib/Drupal/Core/Form/FormSubmitter.php b/core/lib/Drupal/Core/Form/FormSubmitter.php
index ab7e76e..608c762 100644
--- a/core/lib/Drupal/Core/Form/FormSubmitter.php
+++ b/core/lib/Drupal/Core/Form/FormSubmitter.php
@@ -79,12 +79,12 @@ public function doSubmitForm(&$form, FormStateInterface &$form_state) {
     $form_state['executed'] = TRUE;
 
     // If no response has been set, process the form redirect.
-    if (!$form_state->has('response') && $redirect = $this->redirectForm($form_state)) {
+    if (!$form_state->getResponse() && $redirect = $this->redirectForm($form_state)) {
       $form_state->setResponse($redirect);
     }
 
     // If there is a response was set, return it instead of continuing.
-    if (($response = $form_state->get('response')) && $response instanceof Response) {
+    if (($response = $form_state->getResponse()) && $response instanceof Response) {
       return $response;
     }
   }
diff --git a/core/lib/Drupal/Core/Form/FormValidator.php b/core/lib/Drupal/Core/Form/FormValidator.php
index 5efedb0..05d5edf 100644
--- a/core/lib/Drupal/Core/Form/FormValidator.php
+++ b/core/lib/Drupal/Core/Form/FormValidator.php
@@ -173,7 +173,7 @@ protected function handleErrorsWithLimitedValidation(&$form, FormStateInterface
           $values[$name] = $button_value;
         }
       }
-      $form_state->set('values', $values);
+      $form_state->setValues($values);
     }
   }
 
diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php
index 3cbe18c..e53493c 100644
--- a/core/lib/Drupal/Core/Render/Element/RenderElement.php
+++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php
@@ -156,14 +156,15 @@ public static function processGroup(&$element, FormStateInterface $form_state, &
 
     // Each details element forms a new group. The #type 'vertical_tabs' basically
     // only injects a new details element.
-    $form_state['groups'][$parents]['#group_exists'] = TRUE;
-    $element['#groups'] = &$form_state['groups'];
+    $groups = &$form_state->getGroups();
+    $groups[$parents]['#group_exists'] = TRUE;
+    $element['#groups'] = &$groups;
 
     // Process vertical tabs group member details elements.
     if (isset($element['#group'])) {
       // Add this details element to the defined group (by reference).
       $group = $element['#group'];
-      $form_state['groups'][$group][] = &$element;
+      $groups[$group][] = &$element;
     }
 
     return $element;
diff --git a/core/modules/aggregator/tests/src/Plugin/AggregatorPluginSettingsBaseTest.php b/core/modules/aggregator/tests/src/Plugin/AggregatorPluginSettingsBaseTest.php
index eb88867..3b8e5d6 100644
--- a/core/modules/aggregator/tests/src/Plugin/AggregatorPluginSettingsBaseTest.php
+++ b/core/modules/aggregator/tests/src/Plugin/AggregatorPluginSettingsBaseTest.php
@@ -74,7 +74,10 @@ protected function setUp() {
    */
   public function testSettingsForm() {
     // Emulate a form state of a sumbitted form.
-    $form_state = new FormState(array('values' => array('dummy_length' => '', 'aggregator_allowed_html_tags' => '')));
+    $form_state = (new FormState())->setValues([
+      'dummy_length' => '',
+      'aggregator_allowed_html_tags' => '',
+    ]);
 
     $test_processor = $this->getMock(
       'Drupal\aggregator_test\Plugin\aggregator\processor\TestProcessor',
diff --git a/core/modules/block/src/BlockForm.php b/core/modules/block/src/BlockForm.php
index e3d442d..b3f7b14 100644
--- a/core/modules/block/src/BlockForm.php
+++ b/core/modules/block/src/BlockForm.php
@@ -149,9 +149,7 @@ public function validate(array $form, FormStateInterface $form_state) {
 
     // The Block Entity form puts all block plugin form elements in the
     // settings form element, so just pass that to the block for validation.
-    $settings = new FormState(array(
-      'values' => $form_state->getValue('settings')
-    ));
+    $settings = (new FormState())->setValues($form_state->getValue('settings'));
     // Call the plugin validate handler.
     $this->entity->getPlugin()->validateConfigurationForm($form, $settings);
     // Update the original form values.
@@ -168,9 +166,7 @@ public function submit(array $form, FormStateInterface $form_state) {
     // The Block Entity form puts all block plugin form elements in the
     // settings form element, so just pass that to the block for submission.
     // @todo Find a way to avoid this manipulation.
-    $settings = new FormState(array(
-      'values' => $form_state->getValue('settings'),
-    ));
+    $settings = (new FormState())->setValues($form_state->getValue('settings'));
 
     // Call the plugin submit handler.
     $entity->getPlugin()->submitConfigurationForm($form, $settings);
diff --git a/core/modules/config/src/Form/ConfigSingleExportForm.php b/core/modules/config/src/Form/ConfigSingleExportForm.php
index 6a22b16..8e30e6f 100644
--- a/core/modules/config/src/Form/ConfigSingleExportForm.php
+++ b/core/modules/config/src/Form/ConfigSingleExportForm.php
@@ -123,10 +123,10 @@ public function buildForm(array $form, FormStateInterface $form_state, $config_t
       '#suffix' => '</div>',
     );
     if ($config_type && $config_name) {
-      $fake_form_state = new FormState(array('values' => array(
+      $fake_form_state = (new FormState())->setValues([
         'config_type' => $config_type,
         'config_name' => $config_name,
-      )));
+      ]);
       $form['export'] = $this->updateExport($form, $fake_form_state);
     }
     return $form;
diff --git a/core/modules/image/src/Form/ImageEffectFormBase.php b/core/modules/image/src/Form/ImageEffectFormBase.php
index 008e0a5..910c781 100644
--- a/core/modules/image/src/Form/ImageEffectFormBase.php
+++ b/core/modules/image/src/Form/ImageEffectFormBase.php
@@ -106,9 +106,7 @@ public function buildForm(array $form, FormStateInterface $form_state, ImageStyl
   public function validateForm(array &$form, FormStateInterface $form_state) {
     // The image effect configuration is stored in the 'data' key in the form,
     // pass that through for validation.
-    $effect_data = new FormState(array(
-      'values' => $form_state->getValue('data'),
-    ));
+    $effect_data = (new FormState())->setValues($form_state->getValue('data'));
     $this->imageEffect->validateConfigurationForm($form, $effect_data);
     // Update the original form values.
     $form_state->setValue('data', $effect_data['values']);
@@ -122,9 +120,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
 
     // The image effect configuration is stored in the 'data' key in the form,
     // pass that through for submission.
-    $effect_data = new FormState(array(
-      'values' => $form_state->getValue('data'),
-    ));
+    $effect_data = (new FormState())->setValues($form_state->getValue('data'));
     $this->imageEffect->submitConfigurationForm($form, $effect_data);
     // Update the original form values.
     $form_state->setValue('data', $effect_data['values']);
diff --git a/core/modules/menu_ui/src/MenuForm.php b/core/modules/menu_ui/src/MenuForm.php
index 58cb663..5c31fae 100644
--- a/core/modules/menu_ui/src/MenuForm.php
+++ b/core/modules/menu_ui/src/MenuForm.php
@@ -214,7 +214,10 @@ protected function buildOverviewForm(array &$form, FormStateInterface $form_stat
     // section.
     $form['#tree'] = TRUE;
     $form['#theme'] = 'menu_overview_form';
-    $form_state->setIfNotExists('menu_overview_form_parents', array());
+
+    if (!$form_state->has('menu_overview_form_parents')) {
+      $form_state->set('menu_overview_form_parents', array());
+    }
 
     $form['#attached']['css'] = array(drupal_get_path('module', 'menu') . '/css/menu.admin.css');
 
diff --git a/core/modules/quickedit/src/QuickEditController.php b/core/modules/quickedit/src/QuickEditController.php
index c17e4e7..1ccf969 100644
--- a/core/modules/quickedit/src/QuickEditController.php
+++ b/core/modules/quickedit/src/QuickEditController.php
@@ -178,13 +178,10 @@ public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view
       $this->tempStoreFactory->get('quickedit')->set($entity->uuid(), $entity);
     }
 
-    $form_state = new FormState(array(
-      'langcode' => $langcode,
-      'no_redirect' => TRUE,
-      'build_info' => array(
-        'args' => array($entity, $field_name),
-      ),
-    ));
+    $form_state = (new FormState())
+      ->set('langcode', $langcode)
+      ->disableRedirect()
+      ->addBuildInfo('args', [$entity, $field_name]);
     $form = $this->formBuilder()->buildForm('Drupal\quickedit\Form\QuickEditFieldForm', $form_state);
 
     if (!empty($form_state['executed'])) {
diff --git a/core/modules/simpletest/src/Form/SimpletestResultsForm.php b/core/modules/simpletest/src/Form/SimpletestResultsForm.php
index ead1767..7f85f65 100644
--- a/core/modules/simpletest/src/Form/SimpletestResultsForm.php
+++ b/core/modules/simpletest/src/Form/SimpletestResultsForm.php
@@ -271,7 +271,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     }
 
     $form_execute = array();
-    $form_state_execute = new FormState(array('values' => array()));
+    $form_state_execute = new FormState();
     foreach ($classes as $class) {
       $form_state_execute['values']['tests'][$class] = $class;
     }
diff --git a/core/modules/system/src/Tests/Form/FormDefaultHandlersTest.php b/core/modules/system/src/Tests/Form/FormDefaultHandlersTest.php
index 51c79f8..d51cc1a 100644
--- a/core/modules/system/src/Tests/Form/FormDefaultHandlersTest.php
+++ b/core/modules/system/src/Tests/Form/FormDefaultHandlersTest.php
@@ -75,7 +75,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
    * Tests that default handlers are added even if custom are specified.
    */
   function testDefaultAndCustomHandlers() {
-    $form_state = new FormState(array('values' => array()));
+    $form_state = new FormState();
     $form_builder = $this->container->get('form_builder');
     $form_builder->submitForm($this, $form_state);
 
diff --git a/core/modules/system/src/Tests/Form/ProgrammaticTest.php b/core/modules/system/src/Tests/Form/ProgrammaticTest.php
index 16c11b3..a2be45c 100644
--- a/core/modules/system/src/Tests/Form/ProgrammaticTest.php
+++ b/core/modules/system/src/Tests/Form/ProgrammaticTest.php
@@ -72,7 +72,7 @@ function testSubmissionWorkflow() {
    */
   private function submitForm($values, $valid_input) {
     // Programmatically submit the given values.
-    $form_state = new FormState(array('values' => $values));
+    $form_state = (new FormState())->setValues($values);
     \Drupal::formBuilder()->submitForm('\Drupal\form_test\Form\FormTestProgrammaticForm', $form_state);
 
     // Check that the form returns an error when expected, and vice versa.
@@ -99,10 +99,10 @@ private function submitForm($values, $valid_input) {
    * Test the programmed_bypass_access_check flag.
    */
   public function testProgrammaticAccessBypass() {
-    $form_state = new FormState(array('values' => array(
+    $form_state = (new FormState())->setValues([
       'textfield' => 'dummy value',
       'field_restricted' => 'dummy value'
-    )));
+    ]);
 
     // Programmatically submit the form with a value for the restricted field.
     // Since programmed_bypass_access_check is set to TRUE by default, the
diff --git a/core/modules/system/src/Tests/System/SystemConfigFormTestBase.php b/core/modules/system/src/Tests/System/SystemConfigFormTestBase.php
index cabd863..9189ca0 100644
--- a/core/modules/system/src/Tests/System/SystemConfigFormTestBase.php
+++ b/core/modules/system/src/Tests/System/SystemConfigFormTestBase.php
@@ -52,7 +52,7 @@ public function testConfigForm() {
     foreach ($this->values as $form_key => $data) {
       $values[$form_key] = $data['#value'];
     }
-    $form_state = new FormState(array('values' => $values));
+    $form_state = (new FormState())->setValues($values);
     \Drupal::formBuilder()->submitForm($this->form, $form_state);
 
     // Check that the form returns an error when expected, and vice versa.
diff --git a/core/modules/system/tests/modules/batch_test/src/Controller/BatchTestController.php b/core/modules/system/tests/modules/batch_test/src/Controller/BatchTestController.php
index fcd9b01..d6829fa 100644
--- a/core/modules/system/tests/modules/batch_test/src/Controller/BatchTestController.php
+++ b/core/modules/system/tests/modules/batch_test/src/Controller/BatchTestController.php
@@ -85,9 +85,9 @@ public function testNoForm() {
    *   Render array containing markup.
    */
   function testProgrammatic($value = 1) {
-    $form_state = new FormState(array(
-      'values' => array('value' => $value)
-    ));
+    $form_state = (new FormState())->setValues([
+      'value' => $value,
+    ]);
     \Drupal::formBuilder()->submitForm('Drupal\batch_test\Form\BatchTestChainedForm', $form_state);
     return array(
       'success' => array(
diff --git a/core/modules/user/src/Tests/UserAccountFormFieldsTest.php b/core/modules/user/src/Tests/UserAccountFormFieldsTest.php
index 492218f..e2f9145 100644
--- a/core/modules/user/src/Tests/UserAccountFormFieldsTest.php
+++ b/core/modules/user/src/Tests/UserAccountFormFieldsTest.php
@@ -31,9 +31,8 @@ class UserAccountFormFieldsTest extends DrupalUnitTestBase {
   function testInstallConfigureForm() {
     require_once DRUPAL_ROOT . '/core/includes/install.core.inc';
     require_once DRUPAL_ROOT . '/core/includes/install.inc';
-    $install_state = install_state_defaults();
     $form_state = new FormState();
-    $form_state['build_info']['args'][] = &$install_state;
+    $form_state['build_info']['args'][] = install_state_defaults();
     $form = $this->container->get('form_builder')
       ->buildForm('Drupal\Core\Installer\Form\SiteConfigureForm', $form_state);
 
diff --git a/core/modules/views/includes/ajax.inc b/core/modules/views/includes/ajax.inc
index da57333..c6daa71 100644
--- a/core/modules/views/includes/ajax.inc
+++ b/core/modules/views/includes/ajax.inc
@@ -17,12 +17,14 @@
  */
 function views_ajax_form_wrapper($form_class, FormStateInterface &$form_state) {
   // This won't override settings already in.
-  $form_state->setIfNotExists('rerender', FALSE);
-  $form_state->setIfNotExists('no_redirect', !empty($form_state['ajax']));
-  $form_state->setIfNotExists('no_cache', TRUE);
-  $form_state->setIfNotExists('build_info', array(
-    'args' => array(),
-  ));
+  if (!$form_state->has('rerender')) {
+    $form_state->set('rerender', FALSE);
+  }
+  // Do not overwrite if the redirect has been disabled.
+  if (!$form_state->isRedirectDisabled()) {
+    $form_state->disableRedirect(!empty($form_state['ajax']));
+  }
+  $form_state->disableCache();
 
   $form = \Drupal::formBuilder()->buildForm($form_class, $form_state);
   $output = drupal_render($form);
diff --git a/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php b/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php
index 211a388..7b59361 100644
--- a/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php
+++ b/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php
@@ -129,14 +129,13 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
    */
   public function renderExposedForm($block = FALSE) {
     // Deal with any exposed filters we may have, before building.
-    $form_state = new FormState(array(
-      'view' => &$this->view,
-      'display' => &$this->view->display_handler->display,
-      'method' => 'get',
-      'rerender' => TRUE,
-      'no_redirect' => TRUE,
-      'always_process' => TRUE,
-    ));
+    $form_state = (new FormState())
+      ->set('view', $this->view)
+      ->set('display', $this->view->display_handler->display)
+      ->set('rerender', TRUE)
+      ->setMethod('get')
+      ->setAlwaysProcess()
+      ->disableRedirect();
 
     // Some types of displays (eg. attachments) may wish to use the exposed
     // filters of their parent displays instead of showing an additional
diff --git a/core/modules/views/src/Tests/Wizard/WizardPluginBaseUnitTest.php b/core/modules/views/src/Tests/Wizard/WizardPluginBaseUnitTest.php
index 72a8823..da145da 100644
--- a/core/modules/views/src/Tests/Wizard/WizardPluginBaseUnitTest.php
+++ b/core/modules/views/src/Tests/Wizard/WizardPluginBaseUnitTest.php
@@ -63,7 +63,7 @@ public function testCreateView() {
     ));
     language_save($language);
 
-    $form_state->set('values', array(
+    $form_state->setValues(array(
       'id' => $random_id,
       'label' => $random_label,
       'description' => $random_description,
diff --git a/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php b/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php
index 15e6ee2..d9d9d63 100644
--- a/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php
+++ b/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php
@@ -66,20 +66,16 @@ protected function setType($type) {
   public function getFormState(ViewStorageInterface $view, $display_id, $js) {
     // $js may already have been converted to a Boolean.
     $ajax = is_string($js) ? $js === 'ajax' : $js;
-    return new FormState(array(
-      'form_id' => $this->getFormId(),
-      'form_key' => $this->getFormKey(),
-      'ajax' => $ajax,
-      'display_id' => $display_id,
-      'view' => $view,
-      'type' => $this->type,
-      'id' => $this->id,
-      'no_redirect' => TRUE,
-      'build_info' => array(
-        'args' => array(),
-        'callback_object' => $this,
-      ),
-    ));
+    return (new FormState())
+      ->set('form_id', $this->getFormId())
+      ->set('form_key', $this->getFormKey())
+      ->set('ajax', $ajax)
+      ->set('display_id', $display_id)
+      ->set('view', $view)
+      ->set('type', $this->type)
+      ->set('id', $this->id)
+      ->disableRedirect()
+      ->addBuildInfo('callback_object', $this);
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
index b680217..e7c842f 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
@@ -123,7 +123,7 @@ public function testHandleFormStateResponse($class, $form_state_key) {
     $form_arg->expects($this->any())
       ->method('submitForm')
       ->will($this->returnCallback(function ($form, FormStateInterface $form_state) use ($response, $form_state_key) {
-        $form_state->set($form_state_key, $response);
+        $form_state->setFormState([$form_state_key => $response]);
       }));
 
     $form_state = new FormState();
@@ -136,7 +136,7 @@ public function testHandleFormStateResponse($class, $form_state_key) {
     catch (\Exception $e) {
       $this->assertSame('exit', $e->getMessage());
     }
-    $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $form_state->get('response'));
+    $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $form_state->getResponse());
   }
 
   /**
@@ -190,7 +190,7 @@ public function testHandleRedirectWithResponse() {
     catch (\Exception $e) {
       $this->assertSame('exit', $e->getMessage());
     }
-    $this->assertSame($response, $form_state->get('response'));
+    $this->assertSame($response, $form_state->getResponse());
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Form/FormStateTest.php b/core/tests/Drupal/Tests/Core/Form/FormStateTest.php
index 0697b2d..5254a8f 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormStateTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormStateTest.php
@@ -29,7 +29,7 @@ class FormStateTest extends UnitTestCase {
    * @dataProvider providerTestGetRedirect
    */
   public function testGetRedirect($form_state_additions, $expected) {
-    $form_state = new FormState($form_state_additions);
+    $form_state = (new FormState())->setFormState($form_state_additions);
     $redirect = $form_state->getRedirect();
     $this->assertEquals($expected, $redirect);
   }
@@ -82,9 +82,9 @@ public function testSetError() {
    */
   public function testGetError($errors, $parents, $error = NULL) {
     $element['#parents'] = $parents;
-    $form_state = new FormState(array(
+    $form_state = (new FormState())->setFormState([
       'errors' => $errors,
-    ));
+    ]);
     $this->assertSame($error, $form_state->getError($element));
   }
 
@@ -110,9 +110,9 @@ public function providerTestGetError() {
    */
   public function testSetErrorByName($limit_validation_errors, $expected_errors, $set_message = FALSE) {
     $form_state = $this->getMockBuilder('Drupal\Core\Form\FormState')
-      ->setConstructorArgs(array(array('limit_validation_errors' => $limit_validation_errors)))
       ->setMethods(array('drupalSetMessage'))
       ->getMock();
+    $form_state->setLimitValidationErrors($limit_validation_errors);
     $form_state->clearErrors();
     $form_state->expects($set_message ? $this->once() : $this->never())
       ->method('drupalSetMessage');
@@ -147,9 +147,9 @@ public function providerTestSetErrorByName() {
    */
   public function testFormErrorsDuringSubmission() {
     $form_state = $this->getMockBuilder('Drupal\Core\Form\FormState')
-      ->setConstructorArgs(array(array('validation_complete' => TRUE)))
       ->setMethods(array('drupalSetMessage'))
       ->getMock();
+    $form_state->setValidationComplete();
     $form_state->setErrorByName('test', 'message');
   }
 
@@ -183,12 +183,12 @@ public function testSetValueForElement() {
    * @dataProvider providerTestGetValue
    */
   public function testGetValue($key, $expected, $default = NULL) {
-    $form_state = new FormState(array('values' => array(
+    $form_state = (new FormState())->setValues([
       'foo' => 'one',
       'bar' => array(
         'baz' => 'two',
       ),
-    )));
+    ]);
     $this->assertSame($expected, $form_state->getValue($key, $default));
   }
 
@@ -215,7 +215,9 @@ public function providerTestGetValue() {
    * @dataProvider providerTestSetValue
    */
   public function testSetValue($key, $value, $expected) {
-    $form_state = new FormState(array('values' => array('bar' => 'wrong')));
+    $form_state = (new FormState())->setValues([
+      'bar' => 'wrong',
+    ]);
     $form_state->setValue($key, $value);
     $this->assertSame($expected, $form_state->getValues());
   }
@@ -272,7 +274,7 @@ public function providerTestSetValue() {
    * @dataProvider providerTestHasValue
    */
   public function testHasValue($key, $expected) {
-    $form_state = new FormState(array('values' => array(
+    $form_state = (new FormState())->setValues([
       'foo' => 'one',
       'bar' => array(
         'baz' => 'two',
@@ -280,7 +282,7 @@ public function testHasValue($key, $expected) {
       'true' => TRUE,
       'false' => FALSE,
       'null' => NULL,
-    )));
+    ]);
     $this->assertSame($expected, $form_state->hasValue($key));
   }
 
@@ -313,7 +315,7 @@ public function providerTestHasValue() {
    * @dataProvider providerTestIsValueEmpty
    */
   public function testIsValueEmpty($key, $expected) {
-    $form_state = new FormState(array('values' => array(
+    $form_state = (new FormState())->setValues([
       'foo' => 'one',
       'bar' => array(
         'baz' => 'two',
@@ -321,7 +323,7 @@ public function testIsValueEmpty($key, $expected) {
       'true' => TRUE,
       'false' => FALSE,
       'null' => NULL,
-    )));
+    ]);
     $this->assertSame($expected, $form_state->isValueEmpty($key));
   }
 
@@ -405,22 +407,73 @@ public function testLoadIncludeAlreadyLoaded() {
     $module = 'some_module';
     $name = 'some_name';
     $form_state = $this->getMockBuilder('Drupal\Core\Form\FormState')
-      ->setConstructorArgs([['build_info' => ['files' => [
-        'some_module:some_name.some_type' => [
-          'type' => $type,
-          'module' => $module,
-          'name' => $name,
-        ],
-      ]]]])
       ->setMethods(array('moduleLoadInclude'))
       ->getMock();
 
+    $form_state->addBuildInfo('files', [
+      'some_module:some_name.some_type' => [
+        'type' => $type,
+        'module' => $module,
+        'name' => $name,
+      ],
+    ]);
     $form_state->expects($this->never())
       ->method('moduleLoadInclude');
 
     $this->assertFalse($form_state->loadInclude($module, $type, $name));
   }
 
+  /**
+   * @covers ::isCached
+   *
+   * @dataProvider providerTestIsCached
+   */
+  public function testIsCached($cache_key, $no_cache_key, $expected) {
+    $form_state = (new FormState())->setFormState([
+      'cache' => $cache_key,
+      'no_cache' => $no_cache_key,
+    ]);
+    $this->assertSame($expected, $form_state->isCached());
+  }
+
+  /**
+   * Provides test data for testIsCached().
+   */
+  public function providerTestIsCached() {
+    $data = [];
+    $data[] = [
+      TRUE,
+      TRUE,
+      FALSE,
+    ];
+    $data[] = [
+      FALSE,
+      TRUE,
+      FALSE,
+    ];
+    $data[] = [
+      FALSE,
+      FALSE,
+      FALSE,
+    ];
+    $data[] = [
+      TRUE,
+      FALSE,
+      TRUE,
+    ];
+    $data[] = [
+      TRUE,
+      NULL,
+      TRUE,
+    ];
+    $data[] = [
+      FALSE,
+      NULL,
+      FALSE,
+    ];
+    return $data;
+  }
+
 }
 
 /**
diff --git a/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php b/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php
index e1c9462..5d53969 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php
@@ -55,10 +55,9 @@ public function testHandleFormSubmissionNotSubmitted() {
   public function testHandleFormSubmissionNoRedirect() {
     $form_submitter = $this->getFormSubmitter();
     $form = array();
-    $form_state = new FormState(array(
-      'submitted' => TRUE,
-      'no_redirect' => TRUE,
-    ));
+    $form_state = (new FormState())
+      ->setSubmitted()
+      ->disableRedirect();
 
     $return = $form_submitter->doSubmitForm($form, $form_state);
     $this->assertTrue($form_state['executed']);
@@ -78,10 +77,9 @@ public function testHandleFormSubmissionWithResponses($class, $form_state_key) {
       ->method('prepare')
       ->will($this->returnValue($response));
 
-    $form_state = new FormState(array(
-      'submitted' => TRUE,
-      $form_state_key => $response,
-    ));
+    $form_state = (new FormState())
+      ->setSubmitted()
+      ->setFormState([$form_state_key => $response]);
 
     $form_submitter = $this->getFormSubmitter();
     $form = array();
diff --git a/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php b/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php
index bc6e081..5ce3171 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php
@@ -182,10 +182,9 @@ public function testHandleErrorsWithLimitedValidation($sections, $triggering_ele
 
     $triggering_element['#limit_validation_errors'] = $sections;
     $form = array();
-    $form_state = new FormState(array(
-      'values' => $values,
-      'triggering_element' => $triggering_element,
-    ));
+    $form_state = (new FormState())
+      ->setValues($values)
+      ->setTriggeringElement($triggering_element);
 
     $form_validator->validateForm('test_form_id', $form, $form_state);
     $this->assertSame($expected, $form_state->getValues());
