Remove successive boxing/unboxing operations

Change-Id: Ia7c11d612565a414188d996b313969568738df64
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/BooleanMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/BooleanMethodOptimizer.java
index 35915cb..100b7b4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/BooleanMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/BooleanMethodOptimizer.java
@@ -6,11 +6,9 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClassAndMethod;
-import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.SingleBoxedBooleanValue;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
 import com.android.tools.r8.ir.code.ConstString;
@@ -19,18 +17,28 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.optimize.library.StatelessLibraryMethodModelCollection;
 import com.android.tools.r8.utils.StringUtils;
 import java.util.Set;
 
-public class BooleanMethodOptimizer extends StatelessLibraryMethodModelCollection {
-
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
+public class BooleanMethodOptimizer extends PrimitiveMethodOptimizer {
 
   BooleanMethodOptimizer(AppView<?> appView) {
-    this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
+    super(appView);
+  }
+
+  @Override
+  DexMethod getBoxMethod() {
+    return dexItemFactory.booleanMembers.valueOf;
+  }
+
+  @Override
+  DexMethod getUnboxMethod() {
+    return dexItemFactory.booleanMembers.booleanValue;
+  }
+
+  @Override
+  boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue) {
+    return abstractValue.isSingleBoxedBoolean();
   }
 
   @Override
@@ -47,31 +55,10 @@
       DexClassAndMethod singleTarget,
       Set<Value> affectedValues,
       Set<BasicBlock> blocksToRemove) {
-    DexMethod singleTargetReference = singleTarget.getReference();
-    if (singleTargetReference.isIdenticalTo(dexItemFactory.booleanMembers.booleanValue)) {
-      optimizeBooleanValue(code, instructionIterator, invoke);
-    } else if (singleTargetReference.isIdenticalTo(dexItemFactory.booleanMembers.parseBoolean)) {
+    if (singleTarget.getReference().isIdenticalTo(dexItemFactory.booleanMembers.parseBoolean)) {
       optimizeParseBoolean(code, instructionIterator, invoke);
-    } else if (singleTargetReference.isIdenticalTo(dexItemFactory.booleanMembers.valueOf)) {
-      optimizeValueOf(code, instructionIterator, invoke, affectedValues);
-    }
-  }
-
-  private void optimizeBooleanValue(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod booleanValueInvoke) {
-    // Optimize Boolean.valueOf(b).booleanValue() into b.
-    AbstractValue abstractValue =
-        booleanValueInvoke.getFirstArgument().getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleBoxedBoolean()) {
-      if (booleanValueInvoke.hasOutValue()) {
-        SingleBoxedBooleanValue singleBoxedBoolean = abstractValue.asSingleBoxedBoolean();
-        instructionIterator.replaceCurrentInstruction(
-            singleBoxedBoolean
-                .toPrimitive(appView.abstractValueFactory())
-                .createMaterializingInstruction(appView, code, booleanValueInvoke));
-      } else {
-        instructionIterator.removeOrReplaceByDebugLocalRead();
-      }
+    } else {
+      optimizeBoxingMethods(code, instructionIterator, invoke, singleTarget, affectedValues);
     }
   }
 
@@ -91,13 +78,14 @@
     }
   }
 
-  private void optimizeValueOf(
+  @Override
+  void optimizeBoxMethod(
       IRCode code,
       InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
+      InvokeMethod boxInvoke,
       Set<Value> affectedValues) {
     // Optimize Boolean.valueOf(b) into Boolean.FALSE or Boolean.TRUE.
-    Value argument = invoke.getFirstOperand();
+    Value argument = boxInvoke.getFirstOperand();
     AbstractValue abstractValue = argument.getAbstractValue(appView, code.context());
     if (abstractValue.isSingleNumberValue()) {
       instructionIterator.replaceCurrentInstructionWithStaticGet(
@@ -107,6 +95,8 @@
               ? dexItemFactory.booleanMembers.TRUE
               : dexItemFactory.booleanMembers.FALSE,
           affectedValues);
+      return;
     }
+    super.optimizeBoxMethod(code, instructionIterator, boxInvoke, affectedValues);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/ByteMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/ByteMethodOptimizer.java
index 0d47dd0..8f97ed0 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/ByteMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/ByteMethodOptimizer.java
@@ -5,64 +5,33 @@
 package com.android.tools.r8.ir.optimize.library.primitive;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClassAndMethod;
-import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.SingleBoxedByteValue;
-import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.BasicBlockIterator;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.InstructionListIterator;
-import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.optimize.library.StatelessLibraryMethodModelCollection;
-import java.util.Set;
 
-public class ByteMethodOptimizer extends StatelessLibraryMethodModelCollection {
-
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
+public class ByteMethodOptimizer extends PrimitiveMethodOptimizer {
 
   ByteMethodOptimizer(AppView<?> appView) {
-    this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
+    super(appView);
+  }
+
+  @Override
+  DexMethod getBoxMethod() {
+    return dexItemFactory.byteMembers.valueOf;
+  }
+
+  @Override
+  DexMethod getUnboxMethod() {
+    return dexItemFactory.byteMembers.byteValue;
+  }
+
+  @Override
+  boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue) {
+    return abstractValue.isSingleBoxedByte();
   }
 
   @Override
   public DexType getType() {
     return dexItemFactory.boxedByteType;
   }
-
-  @Override
-  public void optimize(
-      IRCode code,
-      BasicBlockIterator blockIterator,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove) {
-    if (singleTarget.getReference().isIdenticalTo(dexItemFactory.byteMembers.byteValue)) {
-      optimizeByteValue(code, instructionIterator, invoke);
-    }
-  }
-
-  private void optimizeByteValue(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod byteValueInvoke) {
-    // Optimize Byte.valueOf(b).byteValue() into b.
-    AbstractValue abstractValue =
-        byteValueInvoke.getFirstArgument().getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleBoxedByte()) {
-      if (byteValueInvoke.hasOutValue()) {
-        SingleBoxedByteValue singleBoxedByte = abstractValue.asSingleBoxedByte();
-        instructionIterator.replaceCurrentInstruction(
-            singleBoxedByte
-                .toPrimitive(appView.abstractValueFactory())
-                .createMaterializingInstruction(appView, code, byteValueInvoke));
-      } else {
-        instructionIterator.removeOrReplaceByDebugLocalRead();
-      }
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/CharacterMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/CharacterMethodOptimizer.java
index c443041..689155d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/CharacterMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/CharacterMethodOptimizer.java
@@ -5,64 +5,33 @@
 package com.android.tools.r8.ir.optimize.library.primitive;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClassAndMethod;
-import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.SingleBoxedCharValue;
-import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.BasicBlockIterator;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.InstructionListIterator;
-import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.optimize.library.StatelessLibraryMethodModelCollection;
-import java.util.Set;
 
-public class CharacterMethodOptimizer extends StatelessLibraryMethodModelCollection {
-
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
+public class CharacterMethodOptimizer extends PrimitiveMethodOptimizer {
 
   CharacterMethodOptimizer(AppView<?> appView) {
-    this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
+    super(appView);
+  }
+
+  @Override
+  DexMethod getBoxMethod() {
+    return dexItemFactory.charMembers.valueOf;
+  }
+
+  @Override
+  DexMethod getUnboxMethod() {
+    return dexItemFactory.charMembers.charValue;
+  }
+
+  @Override
+  boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue) {
+    return abstractValue.isSingleBoxedChar();
   }
 
   @Override
   public DexType getType() {
     return dexItemFactory.boxedCharType;
   }
-
-  @Override
-  public void optimize(
-      IRCode code,
-      BasicBlockIterator blockIterator,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove) {
-    if (singleTarget.getReference().isIdenticalTo(dexItemFactory.charMembers.charValue)) {
-      optimizeCharValue(code, instructionIterator, invoke);
-    }
-  }
-
-  private void optimizeCharValue(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod charValueInvoke) {
-    // Optimize Char.valueOf(c).charValue() into c.
-    AbstractValue abstractValue =
-        charValueInvoke.getFirstArgument().getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleBoxedChar()) {
-      if (charValueInvoke.hasOutValue()) {
-        SingleBoxedCharValue singleBoxedChar = abstractValue.asSingleBoxedChar();
-        instructionIterator.replaceCurrentInstruction(
-            singleBoxedChar
-                .toPrimitive(appView.abstractValueFactory())
-                .createMaterializingInstruction(appView, code, charValueInvoke));
-      } else {
-        instructionIterator.removeOrReplaceByDebugLocalRead();
-      }
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/DoubleMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/DoubleMethodOptimizer.java
index 7c95d1c..780cc81 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/DoubleMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/DoubleMethodOptimizer.java
@@ -5,64 +5,33 @@
 package com.android.tools.r8.ir.optimize.library.primitive;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClassAndMethod;
-import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.SingleBoxedDoubleValue;
-import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.BasicBlockIterator;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.InstructionListIterator;
-import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.optimize.library.StatelessLibraryMethodModelCollection;
-import java.util.Set;
 
-public class DoubleMethodOptimizer extends StatelessLibraryMethodModelCollection {
-
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
+public class DoubleMethodOptimizer extends PrimitiveMethodOptimizer {
 
   DoubleMethodOptimizer(AppView<?> appView) {
-    this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
+    super(appView);
+  }
+
+  @Override
+  DexMethod getBoxMethod() {
+    return dexItemFactory.doubleMembers.valueOf;
+  }
+
+  @Override
+  DexMethod getUnboxMethod() {
+    return dexItemFactory.doubleMembers.doubleValue;
+  }
+
+  @Override
+  boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue) {
+    return abstractValue.isSingleBoxedDouble();
   }
 
   @Override
   public DexType getType() {
     return dexItemFactory.boxedDoubleType;
   }
-
-  @Override
-  public void optimize(
-      IRCode code,
-      BasicBlockIterator blockIterator,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove) {
-    if (singleTarget.getReference().isIdenticalTo(dexItemFactory.doubleMembers.doubleValue)) {
-      optimizeDoubleValue(code, instructionIterator, invoke);
-    }
-  }
-
-  private void optimizeDoubleValue(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod doubleValueInvoke) {
-    // Optimize Double.valueOf(d).doubleValue() into d.
-    AbstractValue abstractValue =
-        doubleValueInvoke.getFirstArgument().getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleBoxedDouble()) {
-      if (doubleValueInvoke.hasOutValue()) {
-        SingleBoxedDoubleValue singleBoxedDouble = abstractValue.asSingleBoxedDouble();
-        instructionIterator.replaceCurrentInstruction(
-            singleBoxedDouble
-                .toPrimitive(appView.abstractValueFactory())
-                .createMaterializingInstruction(appView, code, doubleValueInvoke));
-      } else {
-        instructionIterator.removeOrReplaceByDebugLocalRead();
-      }
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/FloatMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/FloatMethodOptimizer.java
index 5af9174..c3206b4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/FloatMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/FloatMethodOptimizer.java
@@ -5,64 +5,33 @@
 package com.android.tools.r8.ir.optimize.library.primitive;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClassAndMethod;
-import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.SingleBoxedFloatValue;
-import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.BasicBlockIterator;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.InstructionListIterator;
-import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.optimize.library.StatelessLibraryMethodModelCollection;
-import java.util.Set;
 
-public class FloatMethodOptimizer extends StatelessLibraryMethodModelCollection {
-
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
+public class FloatMethodOptimizer extends PrimitiveMethodOptimizer {
 
   FloatMethodOptimizer(AppView<?> appView) {
-    this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
+    super(appView);
+  }
+
+  @Override
+  DexMethod getBoxMethod() {
+    return dexItemFactory.floatMembers.valueOf;
+  }
+
+  @Override
+  DexMethod getUnboxMethod() {
+    return dexItemFactory.floatMembers.floatValue;
+  }
+
+  @Override
+  boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue) {
+    return abstractValue.isSingleBoxedFloat();
   }
 
   @Override
   public DexType getType() {
     return dexItemFactory.boxedFloatType;
   }
-
-  @Override
-  public void optimize(
-      IRCode code,
-      BasicBlockIterator blockIterator,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove) {
-    if (singleTarget.getReference().isIdenticalTo(dexItemFactory.floatMembers.floatValue)) {
-      optimizeFloatValue(code, instructionIterator, invoke);
-    }
-  }
-
-  private void optimizeFloatValue(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod floatValueInvoke) {
-    // Optimize Float.valueOf(f).floatValue() into f.
-    AbstractValue abstractValue =
-        floatValueInvoke.getFirstArgument().getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleBoxedFloat()) {
-      if (floatValueInvoke.hasOutValue()) {
-        SingleBoxedFloatValue singleBoxedFloat = abstractValue.asSingleBoxedFloat();
-        instructionIterator.replaceCurrentInstruction(
-            singleBoxedFloat
-                .toPrimitive(appView.abstractValueFactory())
-                .createMaterializingInstruction(appView, code, floatValueInvoke));
-      } else {
-        instructionIterator.removeOrReplaceByDebugLocalRead();
-      }
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/IntegerMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/IntegerMethodOptimizer.java
index 2b16ec1..dba75d4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/IntegerMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/IntegerMethodOptimizer.java
@@ -5,64 +5,33 @@
 package com.android.tools.r8.ir.optimize.library.primitive;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClassAndMethod;
-import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.SingleBoxedIntegerValue;
-import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.BasicBlockIterator;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.InstructionListIterator;
-import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.optimize.library.StatelessLibraryMethodModelCollection;
-import java.util.Set;
 
-public class IntegerMethodOptimizer extends StatelessLibraryMethodModelCollection {
-
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
+public class IntegerMethodOptimizer extends PrimitiveMethodOptimizer {
 
   IntegerMethodOptimizer(AppView<?> appView) {
-    this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
+    super(appView);
+  }
+
+  @Override
+  DexMethod getBoxMethod() {
+    return dexItemFactory.integerMembers.valueOf;
+  }
+
+  @Override
+  DexMethod getUnboxMethod() {
+    return dexItemFactory.integerMembers.intValue;
+  }
+
+  @Override
+  boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue) {
+    return abstractValue.isSingleBoxedInteger();
   }
 
   @Override
   public DexType getType() {
     return dexItemFactory.boxedIntType;
   }
-
-  @Override
-  public void optimize(
-      IRCode code,
-      BasicBlockIterator blockIterator,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove) {
-    if (singleTarget.getReference().isIdenticalTo(dexItemFactory.integerMembers.intValue)) {
-      optimizeIntegerValue(code, instructionIterator, invoke);
-    }
-  }
-
-  private void optimizeIntegerValue(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod intValueInvoke) {
-    // Optimize Integer.valueOf(i).intValue() into i.
-    AbstractValue abstractValue =
-        intValueInvoke.getFirstArgument().getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleBoxedInteger()) {
-      if (intValueInvoke.hasOutValue()) {
-        SingleBoxedIntegerValue singleBoxedInteger = abstractValue.asSingleBoxedInteger();
-        instructionIterator.replaceCurrentInstruction(
-            singleBoxedInteger
-                .toPrimitive(appView.abstractValueFactory())
-                .createMaterializingInstruction(appView, code, intValueInvoke));
-      } else {
-        instructionIterator.removeOrReplaceByDebugLocalRead();
-      }
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/LongMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/LongMethodOptimizer.java
index 375428a..960baaa 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/LongMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/LongMethodOptimizer.java
@@ -5,64 +5,33 @@
 package com.android.tools.r8.ir.optimize.library.primitive;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClassAndMethod;
-import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.SingleBoxedLongValue;
-import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.BasicBlockIterator;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.InstructionListIterator;
-import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.optimize.library.StatelessLibraryMethodModelCollection;
-import java.util.Set;
 
-public class LongMethodOptimizer extends StatelessLibraryMethodModelCollection {
-
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
+public class LongMethodOptimizer extends PrimitiveMethodOptimizer {
 
   LongMethodOptimizer(AppView<?> appView) {
-    this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
+    super(appView);
+  }
+
+  @Override
+  DexMethod getBoxMethod() {
+    return dexItemFactory.longMembers.valueOf;
+  }
+
+  @Override
+  DexMethod getUnboxMethod() {
+    return dexItemFactory.longMembers.longValue;
+  }
+
+  @Override
+  boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue) {
+    return abstractValue.isSingleBoxedLong();
   }
 
   @Override
   public DexType getType() {
     return dexItemFactory.boxedLongType;
   }
-
-  @Override
-  public void optimize(
-      IRCode code,
-      BasicBlockIterator blockIterator,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove) {
-    if (singleTarget.getReference().isIdenticalTo(dexItemFactory.longMembers.longValue)) {
-      optimizeLongValue(code, instructionIterator, invoke);
-    }
-  }
-
-  private void optimizeLongValue(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod longValueInvoke) {
-    // Optimize Long.valueOf(l).longValue() into l.
-    AbstractValue abstractValue =
-        longValueInvoke.getFirstArgument().getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleBoxedLong()) {
-      if (longValueInvoke.hasOutValue()) {
-        SingleBoxedLongValue singleBoxedLong = abstractValue.asSingleBoxedLong();
-        instructionIterator.replaceCurrentInstruction(
-            singleBoxedLong
-                .toPrimitive(appView.abstractValueFactory())
-                .createMaterializingInstruction(appView, code, longValueInvoke));
-      } else {
-        instructionIterator.removeOrReplaceByDebugLocalRead();
-      }
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/PrimitiveMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/PrimitiveMethodOptimizer.java
index 6d49a6c..c121c66 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/PrimitiveMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/PrimitiveMethodOptimizer.java
@@ -5,13 +5,33 @@
 package com.android.tools.r8.ir.optimize.library.primitive;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.SingleBoxedPrimitiveValue;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.optimize.library.StatelessLibraryMethodModelCollection;
+import java.util.Set;
 import java.util.function.Consumer;
 
 public abstract class PrimitiveMethodOptimizer extends StatelessLibraryMethodModelCollection {
 
+  final AppView<?> appView;
+  final DexItemFactory dexItemFactory;
+
+  PrimitiveMethodOptimizer(AppView<?> appView) {
+    this.appView = appView;
+    this.dexItemFactory = appView.dexItemFactory();
+  }
+
   public static void forEachPrimitiveOptimizer(
-      AppView<?> appView, Consumer<StatelessLibraryMethodModelCollection> register) {
+      AppView<?> appView, Consumer<PrimitiveMethodOptimizer> register) {
     register.accept(new BooleanMethodOptimizer(appView));
     register.accept(new ByteMethodOptimizer(appView));
     register.accept(new CharacterMethodOptimizer(appView));
@@ -21,4 +41,83 @@
     register.accept(new LongMethodOptimizer(appView));
     register.accept(new ShortMethodOptimizer(appView));
   }
+
+  abstract DexMethod getBoxMethod();
+
+  abstract DexMethod getUnboxMethod();
+
+  abstract boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue);
+
+  @Override
+  public void optimize(
+      IRCode code,
+      BasicBlockIterator blockIterator,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      Set<Value> affectedValues,
+      Set<BasicBlock> blocksToRemove) {
+    optimizeBoxingMethods(code, instructionIterator, invoke, singleTarget, affectedValues);
+  }
+
+  void optimizeBoxingMethods(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      Set<Value> affectedValues) {
+    if (singleTarget.getReference().isIdenticalTo(getUnboxMethod())) {
+      optimizeUnboxMethod(code, instructionIterator, invoke);
+    } else if (singleTarget.getReference().isIdenticalTo(getBoxMethod())) {
+      optimizeBoxMethod(code, instructionIterator, invoke, affectedValues);
+    }
+  }
+
+  void optimizeBoxMethod(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod boxInvoke,
+      Set<Value> affectedValues) {
+    Value firstArg = boxInvoke.getFirstArgument();
+    if (firstArg
+        .getAliasedValue()
+        .isDefinedByInstructionSatisfying(i -> i.isInvokeMethod(getUnboxMethod()))) {
+      // Optimize Primitive.box(boxed.unbox()) into boxed.
+      InvokeMethod unboxInvoke = firstArg.getAliasedValue().getDefinition().asInvokeMethod();
+      assert unboxInvoke.isInvokeVirtual();
+      Value src = boxInvoke.outValue();
+      Value replacement = unboxInvoke.getFirstArgument();
+      // We need to update affected values if the nullability is different.
+      src.replaceUsers(replacement, affectedValues);
+      instructionIterator.removeOrReplaceByDebugLocalRead();
+    }
+  }
+
+  void optimizeUnboxMethod(
+      IRCode code, InstructionListIterator instructionIterator, InvokeMethod unboxInvoke) {
+    Value firstArg = unboxInvoke.getFirstArgument();
+    AbstractValue abstractValue = firstArg.getAbstractValue(appView, code.context());
+    if (isMatchingSingleBoxedPrimitive(abstractValue)) {
+      // Optimize Primitive.box(cst).unbox() into cst, possibly inter-procedurally.
+      if (unboxInvoke.hasOutValue()) {
+        SingleBoxedPrimitiveValue singleBoxedNumber = abstractValue.asSingleBoxedPrimitive();
+        instructionIterator.replaceCurrentInstruction(
+            singleBoxedNumber
+                .toPrimitive(appView.abstractValueFactory())
+                .createMaterializingInstruction(appView, code, unboxInvoke));
+      } else {
+        instructionIterator.removeOrReplaceByDebugLocalRead();
+      }
+      return;
+    }
+    if (firstArg
+        .getAliasedValue()
+        .isDefinedByInstructionSatisfying(i -> i.isInvokeMethod(getBoxMethod()))) {
+      // Optimize Primitive.box(unboxed).unbox() into unboxed.
+      InvokeMethod boxInvoke = firstArg.getAliasedValue().getDefinition().asInvokeMethod();
+      assert boxInvoke.isInvokeStatic();
+      unboxInvoke.outValue().replaceUsers(boxInvoke.getFirstArgument());
+      instructionIterator.replaceCurrentInstructionByNullCheckIfPossible(appView, code.context());
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/ShortMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/ShortMethodOptimizer.java
index df1f82e..8bae65e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/ShortMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/primitive/ShortMethodOptimizer.java
@@ -3,66 +3,34 @@
 // BSD-style license that can be found in the LICENSE file.
 
 package com.android.tools.r8.ir.optimize.library.primitive;
-
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClassAndMethod;
-import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.ir.analysis.value.SingleBoxedShortValue;
-import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.BasicBlockIterator;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.InstructionListIterator;
-import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.optimize.library.StatelessLibraryMethodModelCollection;
-import java.util.Set;
 
-public class ShortMethodOptimizer extends StatelessLibraryMethodModelCollection {
-
-  private final AppView<?> appView;
-  private final DexItemFactory dexItemFactory;
+public class ShortMethodOptimizer extends PrimitiveMethodOptimizer {
 
   ShortMethodOptimizer(AppView<?> appView) {
-    this.appView = appView;
-    this.dexItemFactory = appView.dexItemFactory();
+    super(appView);
+  }
+
+  @Override
+  DexMethod getBoxMethod() {
+    return dexItemFactory.shortMembers.valueOf;
+  }
+
+  @Override
+  DexMethod getUnboxMethod() {
+    return dexItemFactory.shortMembers.shortValue;
+  }
+
+  @Override
+  boolean isMatchingSingleBoxedPrimitive(AbstractValue abstractValue) {
+    return abstractValue.isSingleBoxedShort();
   }
 
   @Override
   public DexType getType() {
     return dexItemFactory.boxedShortType;
   }
-
-  @Override
-  public void optimize(
-      IRCode code,
-      BasicBlockIterator blockIterator,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove) {
-    if (singleTarget.getReference().isIdenticalTo(dexItemFactory.shortMembers.shortValue)) {
-      optimizeShortValue(code, instructionIterator, invoke);
-    }
-  }
-
-  private void optimizeShortValue(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod shortValueInvoke) {
-    // Optimize Short.valueOf(s).shortValue() into s.
-    AbstractValue abstractValue =
-        shortValueInvoke.getFirstArgument().getAbstractValue(appView, code.context());
-    if (abstractValue.isSingleBoxedShort()) {
-      if (shortValueInvoke.hasOutValue()) {
-        SingleBoxedShortValue singleBoxedShort = abstractValue.asSingleBoxedShort();
-        instructionIterator.replaceCurrentInstruction(
-            singleBoxedShort
-                .toPrimitive(appView.abstractValueFactory())
-                .createMaterializingInstruction(appView, code, shortValueInvoke));
-      } else {
-        instructionIterator.removeOrReplaceByDebugLocalRead();
-      }
-    }
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/boxedprimitives/ReverseBoxingOperationsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/boxedprimitives/ReverseBoxingOperationsTest.java
new file mode 100644
index 0000000..552abdc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/boxedprimitives/ReverseBoxingOperationsTest.java
@@ -0,0 +1,345 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.boxedprimitives;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ReverseBoxingOperationsTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testUnboxingRemoved() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::assertBoxOperationsRemoved)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(getExpectedResult());
+  }
+
+  private void assertBoxOperationsRemoved(CodeInspector codeInspector) {
+    DexItemFactory factory = codeInspector.getFactory();
+
+    Set<DexMethod> unboxMethods = Sets.newIdentityHashSet();
+    Set<DexMethod> boxMethods = Sets.newIdentityHashSet();
+    for (DexType primitiveType : factory.primitiveToBoxed.keySet()) {
+      unboxMethods.add(factory.getUnboxPrimitiveMethod(primitiveType));
+      boxMethods.add(factory.getBoxPrimitiveMethod(primitiveType));
+    }
+
+    // All box operations should have been removed, except for the unbox methods that act as null
+    // checks, which may or may not be replaced by null-checks depending if they are reprocessed.
+    assertEquals(
+        24,
+        codeInspector
+            .clazz(Main.class)
+            .allMethods(m -> !m.getOriginalName().equals("main"))
+            .size());
+    codeInspector
+        .clazz(Main.class)
+        .allMethods(m -> !m.getOriginalName().equals("main"))
+        .forEach(
+            m ->
+                assertTrue(
+                    m.streamInstructions()
+                        .noneMatch(i -> i.isInvoke() && boxMethods.contains(i.getMethod()))));
+    codeInspector
+        .clazz(Main.class)
+        .allMethods(
+            m ->
+                !m.getOriginalName().equals("main")
+                    && (m.getOriginalName().contains("Unbox")
+                        || m.getOriginalName().contains("NonNull")))
+        .forEach(
+            m ->
+                assertTrue(
+                    m.streamInstructions()
+                        .noneMatch(i -> i.isInvoke() && unboxMethods.contains(i.getMethod()))));
+  }
+
+  private String getExpectedResult() {
+    String[] resultItems = {"1", "1", "1.0", "1.0", "1", "1", "c", "true"};
+    String[] resultItems2 = {"2", "2", "2.0", "2.0", "2", "2", "e", "false"};
+    List<String> result = new ArrayList<>();
+    for (int i = 0; i < resultItems.length; i++) {
+      String item = resultItems[i];
+      String item2 = resultItems2[i];
+      result.add(">" + item);
+      result.add(">" + item);
+      result.add(">npe failure");
+      result.add(">" + item);
+      result.add(">" + item2);
+    }
+    return StringUtils.lines(result);
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      int i = System.currentTimeMillis() > 0 ? 1 : 0;
+      System.out.println(intUnboxTest(i));
+      System.out.println(intTest(i));
+      try {
+        System.out.println(intTest(null));
+      } catch (NullPointerException npe7) {
+        System.out.println("npe failure");
+      }
+      System.out.println(intTestNonNull(i));
+      System.out.println(intTestNonNull(i + 1));
+
+      long l = System.currentTimeMillis() > 0 ? 1L : 0L;
+      System.out.println(longUnboxTest(l));
+      System.out.println(longTest(l));
+      try {
+        System.out.println(longTest(null));
+      } catch (NullPointerException npe6) {
+        System.out.println("npe failure");
+      }
+      System.out.println(longTestNonNull(l));
+      System.out.println(longTestNonNull(l + 1));
+
+      double d = System.currentTimeMillis() > 0 ? 1.0 : 0.0;
+      System.out.println(doubleUnboxTest(d));
+      System.out.println(doubleTest(d));
+      try {
+        System.out.println(doubleTest(null));
+      } catch (NullPointerException npe5) {
+        System.out.println("npe failure");
+      }
+      System.out.println(doubleTestNonNull(d));
+      System.out.println(doubleTestNonNull(d + 1));
+
+      float f = System.currentTimeMillis() > 0 ? 1.0f : 0.0f;
+      System.out.println(floatUnboxTest(f));
+      System.out.println(floatTest(f));
+      try {
+        System.out.println(floatTest(null));
+      } catch (NullPointerException npe4) {
+        System.out.println("npe failure");
+      }
+      System.out.println(floatTestNonNull(f));
+      System.out.println(floatTestNonNull(f + 1));
+
+      byte b = (byte) (System.currentTimeMillis() > 0 ? 1 : 0);
+      System.out.println(byteUnboxTest(b));
+      System.out.println(byteTest(b));
+      try {
+        System.out.println(byteTest(null));
+      } catch (NullPointerException npe3) {
+        System.out.println("npe failure");
+      }
+      System.out.println(byteTestNonNull(b));
+      System.out.println(byteTestNonNull((byte) (b + 1)));
+
+      short s = (short) (System.currentTimeMillis() > 0 ? 1 : 0);
+      System.out.println(shortUnboxTest(s));
+      System.out.println(shortTest(s));
+      try {
+        System.out.println(shortTest(null));
+      } catch (NullPointerException npe2) {
+        System.out.println("npe failure");
+      }
+      System.out.println(shortTestNonNull(s));
+      System.out.println(shortTestNonNull((short) (s + 1)));
+
+      char c = System.currentTimeMillis() > 0 ? 'c' : 'd';
+      System.out.println(charUnboxTest(c));
+      System.out.println(charTest(c));
+      try {
+        System.out.println(charTest(null));
+      } catch (NullPointerException npe1) {
+        System.out.println("npe failure");
+      }
+      System.out.println(charTestNonNull(c));
+      System.out.println(charTestNonNull('e'));
+
+      boolean bool = System.currentTimeMillis() > 0;
+      System.out.println(booleanUnboxTest(bool));
+      System.out.println(booleanTest(bool));
+      try {
+        System.out.println(booleanTest(null));
+      } catch (NullPointerException npe) {
+        System.out.println("npe failure");
+      }
+      System.out.println(booleanTestNonNull(bool));
+      System.out.println(booleanTestNonNull(false));
+    }
+
+    @NeverInline
+    public static int intUnboxTest(int i) {
+      System.out.print(">");
+      return Integer.valueOf(i).intValue();
+    }
+
+    @NeverInline
+    public static Integer intTest(Integer i) {
+      System.out.print(">");
+      return Integer.valueOf(i.intValue());
+    }
+
+    @NeverInline
+    public static Integer intTestNonNull(Integer i) {
+      System.out.print(">");
+      return Integer.valueOf(i.intValue());
+    }
+
+    @NeverInline
+    public static double doubleUnboxTest(double d) {
+      System.out.print(">");
+      return Double.valueOf(d).doubleValue();
+    }
+
+    @NeverInline
+    public static Double doubleTest(Double d) {
+      System.out.print(">");
+      return Double.valueOf(d.doubleValue());
+    }
+
+    @NeverInline
+    public static Double doubleTestNonNull(Double d) {
+      System.out.print(">");
+      return Double.valueOf(d.doubleValue());
+    }
+
+    @NeverInline
+    public static long longUnboxTest(long l) {
+      System.out.print(">");
+      return Long.valueOf(l).longValue();
+    }
+
+    @NeverInline
+    public static Long longTest(Long l) {
+      System.out.print(">");
+      return Long.valueOf(l.longValue());
+    }
+
+    @NeverInline
+    public static Long longTestNonNull(Long l) {
+      System.out.print(">");
+      return Long.valueOf(l.longValue());
+    }
+
+    @NeverInline
+    public static float floatUnboxTest(float f) {
+      System.out.print(">");
+      return Float.valueOf(f).floatValue();
+    }
+
+    @NeverInline
+    public static Float floatTest(Float f) {
+      System.out.print(">");
+      return Float.valueOf(f.floatValue());
+    }
+
+    @NeverInline
+    public static Float floatTestNonNull(Float f) {
+      System.out.print(">");
+      return Float.valueOf(f.floatValue());
+    }
+
+    @NeverInline
+    public static short shortUnboxTest(short s) {
+      System.out.print(">");
+      return Short.valueOf(s).shortValue();
+    }
+
+    @NeverInline
+    public static Short shortTest(Short s) {
+      System.out.print(">");
+      return Short.valueOf(s.shortValue());
+    }
+
+    @NeverInline
+    public static Short shortTestNonNull(Short s) {
+      System.out.print(">");
+      return Short.valueOf(s.shortValue());
+    }
+
+    @NeverInline
+    public static char charUnboxTest(char c) {
+      System.out.print(">");
+      return Character.valueOf(c).charValue();
+    }
+
+    @NeverInline
+    public static Character charTest(Character c) {
+      System.out.print(">");
+      return Character.valueOf(c.charValue());
+    }
+
+    @NeverInline
+    public static Character charTestNonNull(Character c) {
+      System.out.print(">");
+      return Character.valueOf(c.charValue());
+    }
+
+    @NeverInline
+    public static byte byteUnboxTest(byte b) {
+      System.out.print(">");
+      return Byte.valueOf(b).byteValue();
+    }
+
+    @NeverInline
+    public static Byte byteTest(Byte b) {
+      System.out.print(">");
+      return Byte.valueOf(b.byteValue());
+    }
+
+    @NeverInline
+    public static Byte byteTestNonNull(Byte b) {
+      System.out.print(">");
+      return Byte.valueOf(b.byteValue());
+    }
+
+    @NeverInline
+    public static boolean booleanUnboxTest(boolean b) {
+      System.out.print(">");
+      return Boolean.valueOf(b).booleanValue();
+    }
+
+    @NeverInline
+    public static Boolean booleanTest(Boolean b) {
+      System.out.print(">");
+      return Boolean.valueOf(b.booleanValue());
+    }
+
+    @NeverInline
+    public static Boolean booleanTestNonNull(Boolean b) {
+      System.out.print(">");
+      return Boolean.valueOf(b.booleanValue());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/boxedprimitives/UnboxToCheckNotNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/boxedprimitives/UnboxToCheckNotNullTest.java
new file mode 100644
index 0000000..47b7834
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/boxedprimitives/UnboxToCheckNotNullTest.java
@@ -0,0 +1,163 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.boxedprimitives;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class UnboxToCheckNotNullTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testValue() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::assertCheckNotNull)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(getExpectedResult());
+  }
+
+  private void assertCheckNotNull(CodeInspector codeInspector) {
+    DexItemFactory factory = codeInspector.getFactory();
+
+    Set<DexMethod> unboxMethods = Sets.newIdentityHashSet();
+    unboxMethods.addAll(factory.unboxPrimitiveMethod.values());
+    Set<DexType> boxedTypes = Sets.newIdentityHashSet();
+    boxedTypes.addAll(factory.primitiveToBoxed.values());
+
+    // All unbox operations should have been replaced by checkNotNull operations.
+    codeInspector
+        .clazz(Main.class)
+        .allMethods(
+            m ->
+                !m.getParameters().isEmpty()
+                    && boxedTypes.contains(
+                        factory.createType(
+                            DescriptorUtils.javaTypeToDescriptor(m.getParameter(0).getTypeName()))))
+        .forEach(
+            m ->
+                assertTrue(
+                    m.streamInstructions()
+                        .noneMatch(i -> i.isInvoke() && unboxMethods.contains(i.getMethod()))));
+  }
+
+  private String getExpectedResult() {
+    List<String> result = new ArrayList<>();
+    for (int i = 0; i < 8; i++) {
+      result.add("run succeeded");
+      result.add("npe failure");
+    }
+    return StringUtils.lines(result);
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      testCheckingNPE(() -> intTest(1));
+      testCheckingNPE(() -> intTest(null));
+
+      testCheckingNPE(() -> longTest(1L));
+      testCheckingNPE(() -> longTest(null));
+
+      testCheckingNPE(() -> doubleTest(1.0));
+      testCheckingNPE(() -> doubleTest(null));
+
+      testCheckingNPE(() -> floatTest(1.0f));
+      testCheckingNPE(() -> floatTest(null));
+
+      testCheckingNPE(() -> byteTest((byte) 1));
+      testCheckingNPE(() -> byteTest(null));
+
+      testCheckingNPE(() -> shortTest((short) 1));
+      testCheckingNPE(() -> shortTest(null));
+
+      testCheckingNPE(() -> charTest('c'));
+      testCheckingNPE(() -> charTest(null));
+
+      testCheckingNPE(() -> booleanTest(true));
+      testCheckingNPE(() -> booleanTest(null));
+    }
+
+    public static void testCheckingNPE(Runnable runnable) {
+      try {
+        runnable.run();
+        System.out.println("run succeeded");
+      } catch (NullPointerException npe) {
+        System.out.println("npe failure");
+      }
+    }
+
+    @NeverInline
+    public static void intTest(Integer i) {
+      i.intValue();
+    }
+
+    @NeverInline
+    public static void doubleTest(Double d) {
+      d.doubleValue();
+    }
+
+    @NeverInline
+    public static void longTest(Long l) {
+      l.longValue();
+    }
+
+    @NeverInline
+    public static void floatTest(Float f) {
+      f.floatValue();
+    }
+
+    @NeverInline
+    public static void shortTest(Short s) {
+      s.shortValue();
+    }
+
+    @NeverInline
+    public static void charTest(Character c) {
+      c.charValue();
+    }
+
+    @NeverInline
+    public static void byteTest(Byte b) {
+      b.byteValue();
+    }
+
+    @NeverInline
+    public static void booleanTest(Boolean b) {
+      b.booleanValue();
+    }
+  }
+}