am 2e6d4ae0: am f39998c2: am daf63839: Merge "Keep pointers to extension values."

* commit '2e6d4ae02a955d33a75c0642b8ffead20e11fd9a':
  Keep pointers to extension values.
diff --git a/java/README.txt b/java/README.txt
index c693313..4dfef14 100644
--- a/java/README.txt
+++ b/java/README.txt
@@ -442,9 +442,10 @@
 In other words, an appropriate synchronization mechanism (such as
 a ReadWriteLock) must be used to ensure that a message, its
 ancestors, and descendants are not accessed by any other threads
-while the message is being modified. Field reads, getter methods,
-toByteArray(...), writeTo(...), getCachedSize(), and
-getSerializedSize() are all considered read-only operations.
+while the message is being modified. Field reads, getter methods
+(but not getExtension(...)), toByteArray(...), writeTo(...),
+getCachedSize(), and getSerializedSize() are all considered read-only
+operations.
 
 IMPORTANT: If you have fields with defaults and opt out of accessors
 
diff --git a/java/pom.xml b/java/pom.xml
index 5b49a72..0a29f31 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -152,19 +152,19 @@
                   <arg value="../src/google/protobuf/unittest_repeated_merge_nano.proto" />
                 </exec>
                 <exec executable="../src/protoc">
-                  <arg value="--javanano_out=store_unknown_fields=true:target/generated-test-sources" />
+                  <arg value="--javanano_out=store_unknown_fields=true,generate_equals=true:target/generated-test-sources" />
                   <arg value="--proto_path=../src" />
                   <arg value="--proto_path=src/test/java" />
                   <arg value="../src/google/protobuf/unittest_extension_nano.proto" />
                 </exec>
                 <exec executable="../src/protoc">
-                  <arg value="--javanano_out=store_unknown_fields=true:target/generated-test-sources" />
+                  <arg value="--javanano_out=store_unknown_fields=true,generate_equals=true:target/generated-test-sources" />
                   <arg value="--proto_path=../src" />
                   <arg value="--proto_path=src/test/java" />
                   <arg value="../src/google/protobuf/unittest_extension_singular_nano.proto" />
                 </exec>
                 <exec executable="../src/protoc">
-                  <arg value="--javanano_out=store_unknown_fields=true:target/generated-test-sources" />
+                  <arg value="--javanano_out=store_unknown_fields=true,generate_equals=true:target/generated-test-sources" />
                   <arg value="--proto_path=../src" />
                   <arg value="--proto_path=src/test/java" />
                   <arg value="../src/google/protobuf/unittest_extension_repeated_nano.proto" />
diff --git a/java/src/main/java/com/google/protobuf/nano/ExtendableMessageNano.java b/java/src/main/java/com/google/protobuf/nano/ExtendableMessageNano.java
index 63c8afc..a0c2731 100644
--- a/java/src/main/java/com/google/protobuf/nano/ExtendableMessageNano.java
+++ b/java/src/main/java/com/google/protobuf/nano/ExtendableMessageNano.java
@@ -31,8 +31,6 @@
 package com.google.protobuf.nano;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
 
 /**
  * Base class of those Protocol Buffer messages that need to store unknown fields,
@@ -44,27 +42,28 @@
      * A container for fields unknown to the message, including extensions. Extension fields can
      * can be accessed through the {@link #getExtension} and {@link #setExtension} methods.
      */
-    protected List<UnknownFieldData> unknownFieldData;
+    protected FieldArray unknownFieldData;
 
     @Override
     protected int computeSerializedSize() {
         int size = 0;
-        int unknownFieldCount = unknownFieldData == null ? 0 : unknownFieldData.size();
-        for (int i = 0; i < unknownFieldCount; i++) {
-            UnknownFieldData unknownField = unknownFieldData.get(i);
-            size += CodedOutputByteBufferNano.computeRawVarint32Size(unknownField.tag);
-            size += unknownField.bytes.length;
+        if (unknownFieldData != null) {
+            for (int i = 0; i < unknownFieldData.size(); i++) {
+                FieldData field = unknownFieldData.dataAt(i);
+                size += field.computeSerializedSize();
+            }
         }
         return size;
     }
 
     @Override
     public void writeTo(CodedOutputByteBufferNano output) throws IOException {
-        int unknownFieldCount = unknownFieldData == null ? 0 : unknownFieldData.size();
-        for (int i = 0; i < unknownFieldCount; i++) {
-            UnknownFieldData unknownField = unknownFieldData.get(i);
-            output.writeRawVarint32(unknownField.tag);
-            output.writeRawBytes(unknownField.bytes);
+        if (unknownFieldData == null) {
+            return;
+        }
+        for (int i = 0; i < unknownFieldData.size(); i++) {
+            FieldData field = unknownFieldData.dataAt(i);
+            field.writeTo(output);
         }
     }
 
@@ -72,14 +71,38 @@
      * Gets the value stored in the specified extension of this message.
      */
     public final <T> T getExtension(Extension<M, T> extension) {
-        return extension.getValueFrom(unknownFieldData);
+        if (unknownFieldData == null) {
+            return null;
+        }
+        FieldData field = unknownFieldData.get(WireFormatNano.getTagFieldNumber(extension.tag));
+        return field == null ? null : field.getValue(extension);
     }
 
     /**
      * Sets the value of the specified extension of this message.
      */
     public final <T> M setExtension(Extension<M, T> extension, T value) {
-        unknownFieldData = extension.setValueTo(value, unknownFieldData);
+        int fieldNumber = WireFormatNano.getTagFieldNumber(extension.tag);
+        if (value == null) {
+            if (unknownFieldData != null) {
+                unknownFieldData.remove(fieldNumber);
+                if (unknownFieldData.isEmpty()) {
+                    unknownFieldData = null;
+                }
+            }
+        } else {
+            FieldData field = null;
+            if (unknownFieldData == null) {
+                unknownFieldData = new FieldArray();
+            } else {
+                field = unknownFieldData.get(fieldNumber);
+            }
+            if (field == null) {
+                unknownFieldData.put(fieldNumber, new FieldData(extension, value));
+            } else {
+                field.setValue(extension, value);
+            }
+        }
 
         @SuppressWarnings("unchecked") // Generated code should guarantee type safety
         M typedThis = (M) this;
@@ -106,12 +129,22 @@
         if (!input.skipField(tag)) {
             return false;  // This wasn't an unknown field, it's an end-group tag.
         }
-        if (unknownFieldData == null) {
-            unknownFieldData = new ArrayList<UnknownFieldData>();
-        }
+        int fieldNumber = WireFormatNano.getTagFieldNumber(tag);
         int endPos = input.getPosition();
         byte[] bytes = input.getData(startPos, endPos - startPos);
-        unknownFieldData.add(new UnknownFieldData(tag, bytes));
+        UnknownFieldData unknownField = new UnknownFieldData(tag, bytes);
+
+        FieldData field = null;
+        if (unknownFieldData == null) {
+            unknownFieldData = new FieldArray();
+        } else {
+            field = unknownFieldData.get(fieldNumber);
+        }
+        if (field == null) {
+            field = new FieldData();
+            unknownFieldData.put(fieldNumber, field);
+        }
+        field.addUnknownField(unknownField);
         return true;
     }
 }
diff --git a/java/src/main/java/com/google/protobuf/nano/Extension.java b/java/src/main/java/com/google/protobuf/nano/Extension.java
index dfe4f87..962f66e 100644
--- a/java/src/main/java/com/google/protobuf/nano/Extension.java
+++ b/java/src/main/java/com/google/protobuf/nano/Extension.java
@@ -152,58 +152,52 @@
         this.repeated = repeated;
     }
 
-    protected boolean isMatch(int unknownDataTag) {
-        // This implementation is for message/group extensions.
-        return unknownDataTag == tag;
-    }
-
     /**
      * Returns the value of this extension stored in the given list of unknown fields, or
      * {@code null} if no unknown fields matches this extension.
+     *
+     * @param unknownFields a list of {@link UnknownFieldData}. All of the elements must have a tag
+     *                      that matches this Extension's tag.
+     *
      */
     final T getValueFrom(List<UnknownFieldData> unknownFields) {
         if (unknownFields == null) {
             return null;
         }
+        return repeated ? getRepeatedValueFrom(unknownFields) : getSingularValueFrom(unknownFields);
+    }
 
-        if (repeated) {
-            // For repeated extensions, read all matching unknown fields in their original order.
-            List<Object> resultList = new ArrayList<Object>();
-            for (int i = 0; i < unknownFields.size(); i++) {
-                UnknownFieldData data = unknownFields.get(i);
-                if (isMatch(data.tag) && data.bytes.length != 0) {
-                    readDataInto(data, resultList);
-                }
+    private T getRepeatedValueFrom(List<UnknownFieldData> unknownFields) {
+        // For repeated extensions, read all matching unknown fields in their original order.
+        List<Object> resultList = new ArrayList<Object>();
+        for (int i = 0; i < unknownFields.size(); i++) {
+            UnknownFieldData data = unknownFields.get(i);
+            if (data.bytes.length != 0) {
+                readDataInto(data, resultList);
             }
+        }
 
-            int resultSize = resultList.size();
-            if (resultSize == 0) {
-                return null;
-            }
-
+        int resultSize = resultList.size();
+        if (resultSize == 0) {
+            return null;
+        } else {
             T result = clazz.cast(Array.newInstance(clazz.getComponentType(), resultSize));
             for (int i = 0; i < resultSize; i++) {
                 Array.set(result, i, resultList.get(i));
             }
             return result;
-        } else {
-            // For singular extensions, get the last piece of data stored under this extension.
-            UnknownFieldData lastData = null;
-            for (int i = unknownFields.size() - 1; lastData == null && i >= 0; i--) {
-                UnknownFieldData data = unknownFields.get(i);
-                if (isMatch(data.tag) && data.bytes.length != 0) {
-                    lastData = data;
-                }
-            }
-
-            if (lastData == null) {
-                return null;
-            }
-
-            return clazz.cast(readData(CodedInputByteBufferNano.newInstance(lastData.bytes)));
         }
     }
 
+    private T getSingularValueFrom(List<UnknownFieldData> unknownFields) {
+        // For singular extensions, get the last piece of data stored under this extension.
+        if (unknownFields.isEmpty()) {
+            return null;
+        }
+        UnknownFieldData lastData = unknownFields.get(unknownFields.size() - 1);
+        return clazz.cast(readData(CodedInputByteBufferNano.newInstance(lastData.bytes)));
+    }
+
     protected Object readData(CodedInputByteBufferNano input) {
         // This implementation is for message/group extensions.
         Class<?> messageType = repeated ? clazz.getComponentType() : clazz;
@@ -236,61 +230,29 @@
         resultList.add(readData(CodedInputByteBufferNano.newInstance(data.bytes)));
     }
 
-    /**
-     * Sets the value of this extension to the given list of unknown fields. This removes any
-     * previously stored data matching this extension.
-     *
-     * @param value The value of this extension, or {@code null} to clear this extension from the
-     *     unknown fields.
-     * @return The same {@code unknownFields} list, or a new list storing the extension value if
-     *     the argument was null.
-     */
-    final List<UnknownFieldData> setValueTo(T value, List<UnknownFieldData> unknownFields) {
-        if (unknownFields != null) {
-            // Delete all data matching this extension
-            for (int i = unknownFields.size() - 1; i >= 0; i--) {
-                if (isMatch(unknownFields.get(i).tag)) {
-                    unknownFields.remove(i);
-                }
-            }
+    void writeTo(Object value, CodedOutputByteBufferNano output) throws IOException {
+        if (repeated) {
+            writeRepeatedData(value, output);
+        } else {
+            writeSingularData(value, output);
         }
-
-        if (value != null) {
-            if (unknownFields == null) {
-                unknownFields = new ArrayList<UnknownFieldData>();
-            }
-            if (repeated) {
-                writeDataInto(value, unknownFields);
-            } else {
-                unknownFields.add(writeData(value));
-            }
-        }
-
-        // After deletion or no-op addition (due to 'value' being an array of empty or
-        // null-only elements), unknownFields may be empty. Discard the ArrayList if so.
-        return (unknownFields == null || unknownFields.isEmpty()) ? null : unknownFields;
     }
 
-    protected UnknownFieldData writeData(Object value) {
+    protected void writeSingularData(Object value, CodedOutputByteBufferNano out) {
         // This implementation is for message/group extensions.
-        byte[] data;
         try {
+            out.writeRawVarint32(tag);
             switch (type) {
                 case TYPE_GROUP:
                     MessageNano groupValue = (MessageNano) value;
                     int fieldNumber = WireFormatNano.getTagFieldNumber(tag);
-                    data = new byte[CodedOutputByteBufferNano.computeGroupSizeNoTag(groupValue)
-                            + CodedOutputByteBufferNano.computeTagSize(fieldNumber)];
-                    CodedOutputByteBufferNano out = CodedOutputByteBufferNano.newInstance(data);
                     out.writeGroupNoTag(groupValue);
                     // The endgroup tag must be included in the data payload.
                     out.writeTag(fieldNumber, WireFormatNano.WIRETYPE_END_GROUP);
                     break;
                 case TYPE_MESSAGE:
                     MessageNano messageValue = (MessageNano) value;
-                    data = new byte[
-                            CodedOutputByteBufferNano.computeMessageSizeNoTag(messageValue)];
-                    CodedOutputByteBufferNano.newInstance(data).writeMessageNoTag(messageValue);
+                    out.writeMessageNoTag(messageValue);
                     break;
                 default:
                     throw new IllegalArgumentException("Unknown type " + type);
@@ -299,20 +261,55 @@
             // Should not happen
             throw new IllegalStateException(e);
         }
-        return new UnknownFieldData(tag, data);
     }
 
-    protected void writeDataInto(T array, List<UnknownFieldData> unknownFields) {
+    protected void writeRepeatedData(Object array, CodedOutputByteBufferNano output) {
         // This implementation is for non-packed extensions.
         int arrayLength = Array.getLength(array);
         for (int i = 0; i < arrayLength; i++) {
             Object element = Array.get(array, i);
             if (element != null) {
-                unknownFields.add(writeData(element));
+                writeSingularData(element, output);
             }
         }
     }
 
+    int computeSerializedSize(Object value) {
+        if (repeated) {
+            return computeRepeatedSerializedSize(value);
+        } else {
+            return computeSingularSerializedSize(value);
+        }
+    }
+
+    protected int computeRepeatedSerializedSize(Object array) {
+        // This implementation is for non-packed extensions.
+        int size = 0;
+        int arrayLength = Array.getLength(array);
+        for (int i = 0; i < arrayLength; i++) {
+            Object element = Array.get(array, i);
+            if (element != null) {
+                size += computeSingularSerializedSize(Array.get(array, i));
+            }
+        }
+        return size;
+    }
+
+    protected int computeSingularSerializedSize(Object value) {
+        // This implementation is for message/group extensions.
+        int fieldNumber = WireFormatNano.getTagFieldNumber(tag);
+        switch (type) {
+            case TYPE_GROUP:
+                MessageNano groupValue = (MessageNano) value;
+                return CodedOutputByteBufferNano.computeGroupSize(fieldNumber, groupValue);
+            case TYPE_MESSAGE:
+                MessageNano messageValue = (MessageNano) value;
+                return CodedOutputByteBufferNano.computeMessageSize(fieldNumber, messageValue);
+            default:
+                throw new IllegalArgumentException("Unknown type " + type);
+        }
+    }
+
     /**
      * Represents an extension of a primitive (including enum) type. If there is no primitive
      * extensions, this subclass will be removable by ProGuard.
@@ -339,15 +336,6 @@
         }
 
         @Override
-        protected boolean isMatch(int unknownDataTag) {
-            if (repeated) {
-                return unknownDataTag == nonPackedTag || unknownDataTag == packedTag;
-            } else {
-                return unknownDataTag == tag;
-            }
-        }
-
-        @Override
         protected Object readData(CodedInputByteBufferNano input) {
             try {
                 switch (type) {
@@ -398,7 +386,8 @@
             if (data.tag == nonPackedTag) {
                 resultList.add(readData(CodedInputByteBufferNano.newInstance(data.bytes)));
             } else {
-                CodedInputByteBufferNano buffer = CodedInputByteBufferNano.newInstance(data.bytes);
+                CodedInputByteBufferNano buffer =
+                        CodedInputByteBufferNano.newInstance(data.bytes);
                 try {
                     buffer.pushLimit(buffer.readRawVarint32()); // length limit
                 } catch (IOException e) {
@@ -411,105 +400,73 @@
         }
 
         @Override
-        protected final UnknownFieldData writeData(Object value) {
-            byte[] data;
+        protected final void writeSingularData(Object value, CodedOutputByteBufferNano output) {
             try {
+                output.writeRawVarint32(tag);
                 switch (type) {
                     case TYPE_DOUBLE:
                         Double doubleValue = (Double) value;
-                        data = new byte[
-                                CodedOutputByteBufferNano.computeDoubleSizeNoTag(doubleValue)];
-                        CodedOutputByteBufferNano.newInstance(data).writeDoubleNoTag(doubleValue);
+                        output.writeDoubleNoTag(doubleValue);
                         break;
                     case TYPE_FLOAT:
                         Float floatValue = (Float) value;
-                        data = new byte[
-                                CodedOutputByteBufferNano.computeFloatSizeNoTag(floatValue)];
-                        CodedOutputByteBufferNano.newInstance(data).writeFloatNoTag(floatValue);
+                        output.writeFloatNoTag(floatValue);
                         break;
                     case TYPE_INT64:
                         Long int64Value = (Long) value;
-                        data = new byte[
-                                CodedOutputByteBufferNano.computeInt64SizeNoTag(int64Value)];
-                        CodedOutputByteBufferNano.newInstance(data).writeInt64NoTag(int64Value);
+                        output.writeInt64NoTag(int64Value);
                         break;
                     case TYPE_UINT64:
                         Long uint64Value = (Long) value;
-                        data = new byte[
-                                CodedOutputByteBufferNano.computeUInt64SizeNoTag(uint64Value)];
-                        CodedOutputByteBufferNano.newInstance(data).writeUInt64NoTag(uint64Value);
+                        output.writeUInt64NoTag(uint64Value);
                         break;
                     case TYPE_INT32:
                         Integer int32Value = (Integer) value;
-                        data = new byte[
-                                CodedOutputByteBufferNano.computeInt32SizeNoTag(int32Value)];
-                        CodedOutputByteBufferNano.newInstance(data).writeInt32NoTag(int32Value);
+                        output.writeInt32NoTag(int32Value);
                         break;
                     case TYPE_FIXED64:
                         Long fixed64Value = (Long) value;
-                        data = new byte[
-                                CodedOutputByteBufferNano.computeFixed64SizeNoTag(fixed64Value)];
-                        CodedOutputByteBufferNano.newInstance(data).writeFixed64NoTag(fixed64Value);
+                        output.writeFixed64NoTag(fixed64Value);
                         break;
                     case TYPE_FIXED32:
                         Integer fixed32Value = (Integer) value;
-                        data = new byte[
-                                CodedOutputByteBufferNano.computeFixed32SizeNoTag(fixed32Value)];
-                        CodedOutputByteBufferNano.newInstance(data).writeFixed32NoTag(fixed32Value);
+                        output.writeFixed32NoTag(fixed32Value);
                         break;
                     case TYPE_BOOL:
                         Boolean boolValue = (Boolean) value;
-                        data = new byte[CodedOutputByteBufferNano.computeBoolSizeNoTag(boolValue)];
-                        CodedOutputByteBufferNano.newInstance(data).writeBoolNoTag(boolValue);
+                        output.writeBoolNoTag(boolValue);
                         break;
                     case TYPE_STRING:
                         String stringValue = (String) value;
-                        data = new byte[
-                                CodedOutputByteBufferNano.computeStringSizeNoTag(stringValue)];
-                        CodedOutputByteBufferNano.newInstance(data).writeStringNoTag(stringValue);
+                        output.writeStringNoTag(stringValue);
                         break;
                     case TYPE_BYTES:
                         byte[] bytesValue = (byte[]) value;
-                        data = new byte[
-                                CodedOutputByteBufferNano.computeBytesSizeNoTag(bytesValue)];
-                        CodedOutputByteBufferNano.newInstance(data).writeBytesNoTag(bytesValue);
+                        output.writeBytesNoTag(bytesValue);
                         break;
                     case TYPE_UINT32:
                         Integer uint32Value = (Integer) value;
-                        data = new byte[
-                                CodedOutputByteBufferNano.computeUInt32SizeNoTag(uint32Value)];
-                        CodedOutputByteBufferNano.newInstance(data).writeUInt32NoTag(uint32Value);
+                        output.writeUInt32NoTag(uint32Value);
                         break;
                     case TYPE_ENUM:
                         Integer enumValue = (Integer) value;
-                        data = new byte[CodedOutputByteBufferNano.computeEnumSizeNoTag(enumValue)];
-                        CodedOutputByteBufferNano.newInstance(data).writeEnumNoTag(enumValue);
+                        output.writeEnumNoTag(enumValue);
                         break;
                     case TYPE_SFIXED32:
                         Integer sfixed32Value = (Integer) value;
-                        data = new byte[
-                                CodedOutputByteBufferNano.computeSFixed32SizeNoTag(sfixed32Value)];
-                        CodedOutputByteBufferNano.newInstance(data)
-                                .writeSFixed32NoTag(sfixed32Value);
+                        output.writeSFixed32NoTag(sfixed32Value);
                         break;
                     case TYPE_SFIXED64:
                         Long sfixed64Value = (Long) value;
-                        data = new byte[
-                                CodedOutputByteBufferNano.computeSFixed64SizeNoTag(sfixed64Value)];
-                        CodedOutputByteBufferNano.newInstance(data)
-                                .writeSFixed64NoTag(sfixed64Value);
+                        output.writeSFixed64NoTag(sfixed64Value);
                         break;
                     case TYPE_SINT32:
                         Integer sint32Value = (Integer) value;
-                        data = new byte[
-                                CodedOutputByteBufferNano.computeSInt32SizeNoTag(sint32Value)];
-                        CodedOutputByteBufferNano.newInstance(data).writeSInt32NoTag(sint32Value);
+                        output.writeSInt32NoTag(sint32Value);
                         break;
                     case TYPE_SINT64:
                         Long sint64Value = (Long) value;
-                        data = new byte[
-                                CodedOutputByteBufferNano.computeSInt64SizeNoTag(sint64Value)];
-                        CodedOutputByteBufferNano.newInstance(data).writeSInt64NoTag(sint64Value);
+                        output.writeSInt64NoTag(sint64Value);
                         break;
                     default:
                         throw new IllegalArgumentException("Unknown type " + type);
@@ -518,86 +475,21 @@
                 // Should not happen
                 throw new IllegalStateException(e);
             }
-            return new UnknownFieldData(tag, data);
         }
 
         @Override
-        protected void writeDataInto(T array, List<UnknownFieldData> unknownFields) {
+        protected void writeRepeatedData(Object array, CodedOutputByteBufferNano output) {
             if (tag == nonPackedTag) {
                 // Use base implementation for non-packed data
-                super.writeDataInto(array, unknownFields);
+                super.writeRepeatedData(array, output);
             } else if (tag == packedTag) {
                 // Packed. Note that the array element type is guaranteed to be primitive, so there
-                // won't be any null elements, so no null check in this block. First get data size.
+                // won't be any null elements, so no null check in this block.
                 int arrayLength = Array.getLength(array);
-                int dataSize = 0;
-                switch (type) {
-                    case TYPE_BOOL:
-                        // Bools are stored as int32 but just as 0 or 1, so 1 byte each.
-                        dataSize = arrayLength;
-                        break;
-                    case TYPE_FIXED32:
-                    case TYPE_SFIXED32:
-                    case TYPE_FLOAT:
-                        dataSize = arrayLength * CodedOutputByteBufferNano.LITTLE_ENDIAN_32_SIZE;
-                        break;
-                    case TYPE_FIXED64:
-                    case TYPE_SFIXED64:
-                    case TYPE_DOUBLE:
-                        dataSize = arrayLength * CodedOutputByteBufferNano.LITTLE_ENDIAN_64_SIZE;
-                        break;
-                    case TYPE_INT32:
-                        for (int i = 0; i < arrayLength; i++) {
-                            dataSize += CodedOutputByteBufferNano.computeInt32SizeNoTag(
-                                    Array.getInt(array, i));
-                        }
-                        break;
-                    case TYPE_SINT32:
-                        for (int i = 0; i < arrayLength; i++) {
-                            dataSize += CodedOutputByteBufferNano.computeSInt32SizeNoTag(
-                                    Array.getInt(array, i));
-                        }
-                        break;
-                    case TYPE_UINT32:
-                        for (int i = 0; i < arrayLength; i++) {
-                            dataSize += CodedOutputByteBufferNano.computeUInt32SizeNoTag(
-                                    Array.getInt(array, i));
-                        }
-                        break;
-                    case TYPE_INT64:
-                        for (int i = 0; i < arrayLength; i++) {
-                            dataSize += CodedOutputByteBufferNano.computeInt64SizeNoTag(
-                                    Array.getLong(array, i));
-                        }
-                        break;
-                    case TYPE_SINT64:
-                        for (int i = 0; i < arrayLength; i++) {
-                            dataSize += CodedOutputByteBufferNano.computeSInt64SizeNoTag(
-                                    Array.getLong(array, i));
-                        }
-                        break;
-                    case TYPE_UINT64:
-                        for (int i = 0; i < arrayLength; i++) {
-                            dataSize += CodedOutputByteBufferNano.computeUInt64SizeNoTag(
-                                    Array.getLong(array, i));
-                        }
-                        break;
-                    case TYPE_ENUM:
-                        for (int i = 0; i < arrayLength; i++) {
-                            dataSize += CodedOutputByteBufferNano.computeEnumSizeNoTag(
-                                    Array.getInt(array, i));
-                        }
-                        break;
-                    default:
-                        throw new IllegalArgumentException("Unexpected non-packable type " + type);
-                }
+                int dataSize = computePackedDataSize(array);
 
-                // Then construct payload.
-                int payloadSize =
-                        dataSize + CodedOutputByteBufferNano.computeRawVarint32Size(dataSize);
-                byte[] data = new byte[payloadSize];
-                CodedOutputByteBufferNano output = CodedOutputByteBufferNano.newInstance(data);
                 try {
+                    output.writeRawVarint32(tag);
                     output.writeRawVarint32(dataSize);
                     switch (type) {
                         case TYPE_BOOL:
@@ -677,12 +569,154 @@
                     // Should not happen.
                     throw new IllegalStateException(e);
                 }
-                unknownFields.add(new UnknownFieldData(tag, data));
             } else {
                 throw new IllegalArgumentException("Unexpected repeated extension tag " + tag
                         + ", unequal to both non-packed variant " + nonPackedTag
                         + " and packed variant " + packedTag);
             }
         }
+
+        private int computePackedDataSize(Object array) {
+            int dataSize = 0;
+            int arrayLength = Array.getLength(array);
+            switch (type) {
+                case TYPE_BOOL:
+                    // Bools are stored as int32 but just as 0 or 1, so 1 byte each.
+                    dataSize = arrayLength;
+                    break;
+                case TYPE_FIXED32:
+                case TYPE_SFIXED32:
+                case TYPE_FLOAT:
+                    dataSize = arrayLength * CodedOutputByteBufferNano.LITTLE_ENDIAN_32_SIZE;
+                    break;
+                case TYPE_FIXED64:
+                case TYPE_SFIXED64:
+                case TYPE_DOUBLE:
+                    dataSize = arrayLength * CodedOutputByteBufferNano.LITTLE_ENDIAN_64_SIZE;
+                    break;
+                case TYPE_INT32:
+                    for (int i = 0; i < arrayLength; i++) {
+                        dataSize += CodedOutputByteBufferNano.computeInt32SizeNoTag(
+                                Array.getInt(array, i));
+                    }
+                    break;
+                case TYPE_SINT32:
+                    for (int i = 0; i < arrayLength; i++) {
+                        dataSize += CodedOutputByteBufferNano.computeSInt32SizeNoTag(
+                                Array.getInt(array, i));
+                    }
+                    break;
+                case TYPE_UINT32:
+                    for (int i = 0; i < arrayLength; i++) {
+                        dataSize += CodedOutputByteBufferNano.computeUInt32SizeNoTag(
+                                Array.getInt(array, i));
+                    }
+                    break;
+                case TYPE_INT64:
+                    for (int i = 0; i < arrayLength; i++) {
+                        dataSize += CodedOutputByteBufferNano.computeInt64SizeNoTag(
+                                Array.getLong(array, i));
+                    }
+                    break;
+                case TYPE_SINT64:
+                    for (int i = 0; i < arrayLength; i++) {
+                        dataSize += CodedOutputByteBufferNano.computeSInt64SizeNoTag(
+                                Array.getLong(array, i));
+                    }
+                    break;
+                case TYPE_UINT64:
+                    for (int i = 0; i < arrayLength; i++) {
+                        dataSize += CodedOutputByteBufferNano.computeUInt64SizeNoTag(
+                                Array.getLong(array, i));
+                    }
+                    break;
+                case TYPE_ENUM:
+                    for (int i = 0; i < arrayLength; i++) {
+                        dataSize += CodedOutputByteBufferNano.computeEnumSizeNoTag(
+                                Array.getInt(array, i));
+                    }
+                    break;
+                default:
+                    throw new IllegalArgumentException("Unexpected non-packable type " + type);
+            }
+            return dataSize;
+        }
+
+        @Override
+        protected int computeRepeatedSerializedSize(Object array) {
+            if (tag == nonPackedTag) {
+                // Use base implementation for non-packed data
+                return super.computeRepeatedSerializedSize(array);
+            } else if (tag == packedTag) {
+                // Packed.
+                int dataSize = computePackedDataSize(array);
+                int payloadSize =
+                        dataSize + CodedOutputByteBufferNano.computeRawVarint32Size(dataSize);
+                return payloadSize + CodedOutputByteBufferNano.computeRawVarint32Size(tag);
+            } else {
+                throw new IllegalArgumentException("Unexpected repeated extension tag " + tag
+                        + ", unequal to both non-packed variant " + nonPackedTag
+                        + " and packed variant " + packedTag);
+            }
+        }
+
+        @Override
+        protected final int computeSingularSerializedSize(Object value) {
+            int fieldNumber = WireFormatNano.getTagFieldNumber(tag);
+            switch (type) {
+                case TYPE_DOUBLE:
+                    Double doubleValue = (Double) value;
+                    return CodedOutputByteBufferNano.computeDoubleSize(fieldNumber, doubleValue);
+                case TYPE_FLOAT:
+                    Float floatValue = (Float) value;
+                    return CodedOutputByteBufferNano.computeFloatSize(fieldNumber, floatValue);
+                case TYPE_INT64:
+                    Long int64Value = (Long) value;
+                    return CodedOutputByteBufferNano.computeInt64Size(fieldNumber, int64Value);
+                case TYPE_UINT64:
+                    Long uint64Value = (Long) value;
+                    return CodedOutputByteBufferNano.computeUInt64Size(fieldNumber, uint64Value);
+                case TYPE_INT32:
+                    Integer int32Value = (Integer) value;
+                    return CodedOutputByteBufferNano.computeInt32Size(fieldNumber, int32Value);
+                case TYPE_FIXED64:
+                    Long fixed64Value = (Long) value;
+                    return CodedOutputByteBufferNano.computeFixed64Size(fieldNumber, fixed64Value);
+                case TYPE_FIXED32:
+                    Integer fixed32Value = (Integer) value;
+                    return CodedOutputByteBufferNano.computeFixed32Size(fieldNumber, fixed32Value);
+                case TYPE_BOOL:
+                    Boolean boolValue = (Boolean) value;
+                    return CodedOutputByteBufferNano.computeBoolSize(fieldNumber, boolValue);
+                case TYPE_STRING:
+                    String stringValue = (String) value;
+                    return CodedOutputByteBufferNano.computeStringSize(fieldNumber, stringValue);
+                case TYPE_BYTES:
+                    byte[] bytesValue = (byte[]) value;
+                    return CodedOutputByteBufferNano.computeBytesSize(fieldNumber, bytesValue);
+                case TYPE_UINT32:
+                    Integer uint32Value = (Integer) value;
+                    return CodedOutputByteBufferNano.computeUInt32Size(fieldNumber, uint32Value);
+                case TYPE_ENUM:
+                    Integer enumValue = (Integer) value;
+                    return CodedOutputByteBufferNano.computeEnumSize(fieldNumber, enumValue);
+                case TYPE_SFIXED32:
+                    Integer sfixed32Value = (Integer) value;
+                    return CodedOutputByteBufferNano.computeSFixed32Size(fieldNumber,
+                            sfixed32Value);
+                case TYPE_SFIXED64:
+                    Long sfixed64Value = (Long) value;
+                    return CodedOutputByteBufferNano.computeSFixed64Size(fieldNumber,
+                            sfixed64Value);
+                case TYPE_SINT32:
+                    Integer sint32Value = (Integer) value;
+                    return CodedOutputByteBufferNano.computeSInt32Size(fieldNumber, sint32Value);
+                case TYPE_SINT64:
+                    Long sint64Value = (Long) value;
+                    return CodedOutputByteBufferNano.computeSInt64Size(fieldNumber, sint64Value);
+                default:
+                    throw new IllegalArgumentException("Unknown type " + type);
+            }
+        }
     }
 }
diff --git a/java/src/main/java/com/google/protobuf/nano/FieldArray.java b/java/src/main/java/com/google/protobuf/nano/FieldArray.java
new file mode 100644
index 0000000..ab923a4
--- /dev/null
+++ b/java/src/main/java/com/google/protobuf/nano/FieldArray.java
@@ -0,0 +1,273 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2014 Google Inc.  All rights reserved.
+// http://code.google.com/p/protobuf/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package com.google.protobuf.nano;
+
+
+/**
+ * A custom version of {@link android.util.SparseArray} with the minimal API
+ * for storing {@link FieldData} objects.
+ *
+ * Based on {@link android.support.v4.util.SpareArrayCompat}.
+ */
+class FieldArray {
+    private static final FieldData DELETED = new FieldData();
+    private boolean mGarbage = false;
+
+    private int[] mFieldNumbers;
+    private FieldData[] mData;
+    private int mSize;
+
+    /**
+     * Creates a new FieldArray containing no fields.
+     */
+    public FieldArray() {
+        this(10);
+    }
+
+    /**
+     * Creates a new FieldArray containing no mappings that will not
+     * require any additional memory allocation to store the specified
+     * number of mappings.
+     */
+    public FieldArray(int initialCapacity) {
+        initialCapacity = idealIntArraySize(initialCapacity);
+        mFieldNumbers = new int[initialCapacity];
+        mData = new FieldData[initialCapacity];
+        mSize = 0;
+    }
+
+    /**
+     * Gets the FieldData mapped from the specified fieldNumber, or <code>null</code>
+     * if no such mapping has been made.
+     */
+    public FieldData get(int fieldNumber) {
+        int i = binarySearch(fieldNumber);
+
+        if (i < 0 || mData[i] == DELETED) {
+            return null;
+        } else {
+            return mData[i];
+        }
+    }
+
+    /**
+     * Removes the data from the specified fieldNumber, if there was any.
+     */
+    public void remove(int fieldNumber) {
+        int i = binarySearch(fieldNumber);
+
+        if (i >= 0 && mData[i] != DELETED) {
+            mData[i] = DELETED;
+            mGarbage = true;
+        }
+    }
+
+    private void gc() {
+        int n = mSize;
+        int o = 0;
+        int[] keys = mFieldNumbers;
+        FieldData[] values = mData;
+
+        for (int i = 0; i < n; i++) {
+            FieldData val = values[i];
+
+            if (val != DELETED) {
+                if (i != o) {
+                    keys[o] = keys[i];
+                    values[o] = val;
+                    values[i] = null;
+                }
+
+                o++;
+            }
+        }
+
+        mGarbage = false;
+        mSize = o;
+    }
+
+    /**
+     * Adds a mapping from the specified fieldNumber to the specified data,
+     * replacing the previous mapping if there was one.
+     */
+    public void put(int fieldNumber, FieldData data) {
+        int i = binarySearch(fieldNumber);
+
+        if (i >= 0) {
+            mData[i] = data;
+        } else {
+            i = ~i;
+
+            if (i < mSize && mData[i] == DELETED) {
+                mFieldNumbers[i] = fieldNumber;
+                mData[i] = data;
+                return;
+            }
+
+            if (mGarbage && mSize >= mFieldNumbers.length) {
+                gc();
+
+                // Search again because indices may have changed.
+                i = ~ binarySearch(fieldNumber);
+            }
+
+            if (mSize >= mFieldNumbers.length) {
+                int n = idealIntArraySize(mSize + 1);
+
+                int[] nkeys = new int[n];
+                FieldData[] nvalues = new FieldData[n];
+
+                System.arraycopy(mFieldNumbers, 0, nkeys, 0, mFieldNumbers.length);
+                System.arraycopy(mData, 0, nvalues, 0, mData.length);
+
+                mFieldNumbers = nkeys;
+                mData = nvalues;
+            }
+
+            if (mSize - i != 0) {
+                System.arraycopy(mFieldNumbers, i, mFieldNumbers, i + 1, mSize - i);
+                System.arraycopy(mData, i, mData, i + 1, mSize - i);
+            }
+
+            mFieldNumbers[i] = fieldNumber;
+            mData[i] = data;
+            mSize++;
+        }
+    }
+
+    /**
+     * Returns the number of key-value mappings that this FieldArray
+     * currently stores.
+     */
+    public int size() {
+        if (mGarbage) {
+            gc();
+        }
+
+        return mSize;
+    }
+
+    public boolean isEmpty() {
+        return size() == 0;
+    }
+
+    /**
+     * Given an index in the range <code>0...size()-1</code>, returns
+     * the value from the <code>index</code>th key-value mapping that this
+     * FieldArray stores.
+     */
+    public FieldData dataAt(int index) {
+        if (mGarbage) {
+            gc();
+        }
+
+        return mData[index];
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+        if (!(o instanceof FieldArray)) {
+            return false;
+        }
+
+        FieldArray other = (FieldArray) o;
+        if (size() != other.size()) {  // size() will call gc() if necessary.
+            return false;
+        }
+        return arrayEquals(mFieldNumbers, other.mFieldNumbers, mSize) &&
+                arrayEquals(mData, other.mData, mSize);
+    }
+
+    @Override
+    public int hashCode() {
+        if (mGarbage) {
+            gc();
+        }
+        int result = 17;
+        for (int i = 0; i < mSize; i++) {
+            result = 31 * result + mFieldNumbers[i];
+            result = 31 * result + mData[i].hashCode();
+        }
+        return result;
+    }
+
+    private int idealIntArraySize(int need) {
+        return idealByteArraySize(need * 4) / 4;
+    }
+
+    private int idealByteArraySize(int need) {
+        for (int i = 4; i < 32; i++)
+            if (need <= (1 << i) - 12)
+                return (1 << i) - 12;
+
+        return need;
+    }
+
+    private int binarySearch(int value) {
+        int lo = 0;
+        int hi = mSize - 1;
+
+        while (lo <= hi) {
+            int mid = (lo + hi) >>> 1;
+            int midVal = mFieldNumbers[mid];
+
+            if (midVal < value) {
+                lo = mid + 1;
+            } else if (midVal > value) {
+                hi = mid - 1;
+            } else {
+                return mid; // value found
+            }
+        }
+        return ~lo; // value not present
+    }
+
+    private boolean arrayEquals(int[] a, int[] b, int size) {
+        for (int i = 0; i < size; i++) {
+            if (a[i] != b[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean arrayEquals(FieldData[] a, FieldData[] b, int size) {
+        for (int i = 0; i < size; i++) {
+            if (!a[i].equals(b[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+}
diff --git a/java/src/main/java/com/google/protobuf/nano/FieldData.java b/java/src/main/java/com/google/protobuf/nano/FieldData.java
new file mode 100644
index 0000000..7a5eb4c
--- /dev/null
+++ b/java/src/main/java/com/google/protobuf/nano/FieldData.java
@@ -0,0 +1,173 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2014 Google Inc.  All rights reserved.
+// http://code.google.com/p/protobuf/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package com.google.protobuf.nano;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Stores unknown fields. These might be extensions or fields that the generated API doesn't
+ * know about yet.
+ */
+class FieldData {
+    private Extension<?, ?> cachedExtension;
+    private Object value;
+    /** The serialised values for this object. Will be cleared if getValue is called */
+    private List<UnknownFieldData> unknownFieldData;
+
+    <T> FieldData(Extension<?, T> extension, T newValue) {
+        cachedExtension = extension;
+        value = newValue;
+    }
+
+    FieldData() {
+        unknownFieldData = new ArrayList<UnknownFieldData>();
+    }
+
+    void addUnknownField(UnknownFieldData unknownField) {
+        unknownFieldData.add(unknownField);
+    }
+
+    <T> T getValue(Extension<?, T> extension) {
+        if (value != null){
+            if (cachedExtension != extension) {  // Extension objects are singletons.
+                throw new IllegalStateException(
+                        "Tried to getExtension with a differernt Extension.");
+            }
+        } else {
+            cachedExtension = extension;
+            value = extension.getValueFrom(unknownFieldData);
+            unknownFieldData = null;
+        }
+        return (T) value;
+    }
+
+    <T> void setValue(Extension<?, T> extension, T newValue) {
+        cachedExtension = extension;
+        value = newValue;
+        unknownFieldData = null;
+    }
+
+    int computeSerializedSize() {
+        int size = 0;
+        if (value != null) {
+            size = cachedExtension.computeSerializedSize(value);
+        } else {
+            for (UnknownFieldData unknownField : unknownFieldData) {
+                size += unknownField.computeSerializedSize();
+            }
+        }
+        return size;
+    }
+
+    void writeTo(CodedOutputByteBufferNano output) throws IOException {
+        if (value != null) {
+            cachedExtension.writeTo(value, output);
+        } else {
+            for (UnknownFieldData unknownField : unknownFieldData) {
+                unknownField.writeTo(output);
+            }
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+        if (!(o instanceof FieldData)) {
+            return false;
+        }
+
+        FieldData other = (FieldData) o;
+        if (value != null && other.value != null) {
+            // If both objects have deserialized values, compare those.
+            // Since unknown fields are only compared if messages have generated equals methods
+            // we know this will be a meaningful comparison (not identity) for all values.
+            if (cachedExtension != other.cachedExtension) {  // Extension objects are singletons.
+                return false;
+            }
+            if (!cachedExtension.clazz.isArray()) {
+                // Can't test (!cachedExtension.repeated) due to 'bytes' -> 'byte[]'
+                return value.equals(other.value);
+            }
+            if (value instanceof byte[]) {
+                return Arrays.equals((byte[]) value, (byte[]) other.value);
+            } else if (value instanceof int[]) {
+                return Arrays.equals((int[]) value, (int[]) other.value);
+            } else if (value instanceof long[]) {
+                return Arrays.equals((long[]) value, (long[]) other.value);
+            } else if (value instanceof float[]) {
+                return Arrays.equals((float[]) value, (float[]) other.value);
+            } else if (value instanceof double[]) {
+                return Arrays.equals((double[]) value, (double[]) other.value);
+            } else if (value instanceof boolean[]) {
+                return Arrays.equals((boolean[]) value, (boolean[]) other.value);
+            } else {
+                return Arrays.deepEquals((Object[]) value, (Object[]) other.value);
+            }
+        }
+        if (unknownFieldData != null && other.unknownFieldData != null) {
+            // If both objects have byte arrays compare those directly.
+            return unknownFieldData.equals(other.unknownFieldData);
+        }
+        try {
+            // As a last resort, serialize and compare the resulting byte arrays.
+            return Arrays.equals(toByteArray(), other.toByteArray());
+        } catch (IOException e) {
+            // Should not happen.
+            throw new IllegalStateException(e);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 17;
+        try {
+            // The only way to generate a consistent hash is to use the serialized form.
+            result = 31 * result + Arrays.hashCode(toByteArray());
+        } catch (IOException e) {
+            // Should not happen.
+            throw new IllegalStateException(e);
+        }
+        return result;
+    }
+
+    private byte[] toByteArray() throws IOException {
+        byte[] result = new byte[computeSerializedSize()];
+        CodedOutputByteBufferNano output = CodedOutputByteBufferNano.newInstance(result);
+        writeTo(output);
+        return result;
+    }
+
+}
diff --git a/java/src/main/java/com/google/protobuf/nano/UnknownFieldData.java b/java/src/main/java/com/google/protobuf/nano/UnknownFieldData.java
index 833ed2a..2032e1a 100644
--- a/java/src/main/java/com/google/protobuf/nano/UnknownFieldData.java
+++ b/java/src/main/java/com/google/protobuf/nano/UnknownFieldData.java
@@ -30,42 +30,55 @@
 
 package com.google.protobuf.nano;
 
+import java.io.IOException;
 import java.util.Arrays;
 
 /**
- * Stores unknown fields. These might be extensions or fields that the generated API doesn't
- * know about yet.
+ * Stores unknown fields. These might be extensions or fields that the generated
+ * API doesn't know about yet.
  *
  * @author bduff@google.com (Brian Duff)
  */
-public final class UnknownFieldData {
+final class UnknownFieldData {
 
-  final int tag;
-  final byte[] bytes;
+    final int tag;
+    final byte[] bytes;
 
-  UnknownFieldData(int tag, byte[] bytes) {
-    this.tag = tag;
-    this.bytes = bytes;
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o == this) {
-      return true;
-    }
-    if (!(o instanceof UnknownFieldData)) {
-      return false;
+    UnknownFieldData(int tag, byte[] bytes) {
+        this.tag = tag;
+        this.bytes = bytes;
     }
 
-    UnknownFieldData other = (UnknownFieldData) o;
-    return tag == other.tag && Arrays.equals(bytes, other.bytes);
-  }
+    int computeSerializedSize() {
+        int size = 0;
+        size += CodedOutputByteBufferNano.computeRawVarint32Size(tag);
+        size += bytes.length;
+        return size;
+    }
 
-  @Override
-  public int hashCode() {
-    int result = 17;
-    result = 31 * result + tag;
-    result = 31 * result + Arrays.hashCode(bytes);
-    return result;
-  }
+    void writeTo(CodedOutputByteBufferNano output) throws IOException {
+        output.writeRawVarint32(tag);
+        output.writeRawBytes(bytes);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+        if (!(o instanceof UnknownFieldData)) {
+            return false;
+        }
+
+        UnknownFieldData other = (UnknownFieldData) o;
+        return tag == other.tag && Arrays.equals(bytes, other.bytes);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 17;
+        result = 31 * result + tag;
+        result = 31 * result + Arrays.hashCode(bytes);
+        return result;
+    }
 }
diff --git a/java/src/test/java/com/google/protobuf/NanoTest.java b/java/src/test/java/com/google/protobuf/NanoTest.java
index 00e2597..93c9dc4 100644
--- a/java/src/test/java/com/google/protobuf/NanoTest.java
+++ b/java/src/test/java/com/google/protobuf/NanoTest.java
@@ -41,6 +41,7 @@
 import com.google.protobuf.nano.FileScopeEnumMultiple;
 import com.google.protobuf.nano.FileScopeEnumRefNano;
 import com.google.protobuf.nano.InternalNano;
+import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
 import com.google.protobuf.nano.MessageNano;
 import com.google.protobuf.nano.MessageScopeEnumRefNano;
 import com.google.protobuf.nano.MultipleImportingNonMultipleNano1;
@@ -2826,6 +2827,8 @@
     assertEquals(group2.a, message.getExtension(SingularExtensions.someGroup).a);
 
     // Test reading back using RepeatedExtensions: the arrays should be equal.
+    message = Extensions.ExtendableMessage.parseFrom(data);
+    assertEquals(5, message.field);
     assertTrue(Arrays.equals(int32s, message.getExtension(RepeatedExtensions.repeatedInt32)));
     assertTrue(Arrays.equals(uint32s, message.getExtension(RepeatedExtensions.repeatedUint32)));
     assertTrue(Arrays.equals(sint32s, message.getExtension(RepeatedExtensions.repeatedSint32)));
@@ -2860,6 +2863,8 @@
 
     // Test reading back using PackedExtensions: the arrays should be equal, even the fields
     // are non-packed.
+    message = Extensions.ExtendableMessage.parseFrom(data);
+    assertEquals(5, message.field);
     assertTrue(Arrays.equals(int32s, message.getExtension(PackedExtensions.packedInt32)));
     assertTrue(Arrays.equals(uint32s, message.getExtension(PackedExtensions.packedUint32)));
     assertTrue(Arrays.equals(sint32s, message.getExtension(PackedExtensions.packedSint32)));
@@ -2924,6 +2929,138 @@
     assertEquals(0, MessageNano.toByteArray(message).length);
   }
 
+  public void testExtensionsMutation() {
+    Extensions.ExtendableMessage extendableMessage = new Extensions.ExtendableMessage();
+    extendableMessage.setExtension(SingularExtensions.someMessage,
+        new Extensions.AnotherMessage());
+
+    extendableMessage.getExtension(SingularExtensions.someMessage).string = "not empty";
+
+    assertEquals("not empty",
+        extendableMessage.getExtension(SingularExtensions.someMessage).string);
+  }
+
+  public void testExtensionsMutation_Equals() throws InvalidProtocolBufferNanoException {
+    Extensions.ExtendableMessage extendableMessage = new Extensions.ExtendableMessage();
+    extendableMessage.field = 5;
+    int int32 = 42;
+    int[] uint32s = {3, 4};
+    int[] sint32s = {-5, -6};
+    long[] int64s = {7, 8};
+    long[] uint64s = {9, 10};
+    long[] sint64s = {-11, -12};
+    int[] fixed32s = {13, 14};
+    int[] sfixed32s = {-15, -16};
+    long[] fixed64s = {17, 18};
+    long[] sfixed64s = {-19, -20};
+    boolean[] bools = {true, false};
+    float[] floats = {2.1f, 2.2f};
+    double[] doubles = {2.3, 2.4};
+    int[] enums = {Extensions.SECOND_VALUE, Extensions.FIRST_VALUE};
+    String[] strings = {"vijfentwintig", "twenty-six"};
+    byte[][] bytess = {{2, 7}, {2, 8}};
+    AnotherMessage another1 = new AnotherMessage();
+    another1.string = "er shi jiu";
+    another1.value = false;
+    AnotherMessage another2 = new AnotherMessage();
+    another2.string = "trente";
+    another2.value = true;
+    AnotherMessage[] messages = {another1, another2};
+    RepeatedExtensions.RepeatedGroup group1 = new RepeatedExtensions.RepeatedGroup();
+    group1.a = 31;
+    RepeatedExtensions.RepeatedGroup group2 = new RepeatedExtensions.RepeatedGroup();
+    group2.a = 32;
+    RepeatedExtensions.RepeatedGroup[] groups = {group1, group2};
+    extendableMessage.setExtension(SingularExtensions.someInt32, int32);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedUint32, uint32s);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedSint32, sint32s);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedInt64, int64s);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedUint64, uint64s);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedSint64, sint64s);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedFixed32, fixed32s);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedSfixed32, sfixed32s);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedFixed64, fixed64s);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedSfixed64, sfixed64s);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedBool, bools);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedFloat, floats);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedDouble, doubles);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedEnum, enums);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedString, strings);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedBytes, bytess);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedMessage, messages);
+    extendableMessage.setExtension(RepeatedExtensions.repeatedGroup, groups);
+
+    byte[] data = MessageNano.toByteArray(extendableMessage);
+
+    extendableMessage = Extensions.ExtendableMessage.parseFrom(data);
+    Extensions.ExtendableMessage messageCopy = Extensions.ExtendableMessage.parseFrom(data);
+
+    // Without deserialising.
+    assertEquals(extendableMessage, messageCopy);
+    assertEquals(extendableMessage.hashCode(), messageCopy.hashCode());
+
+    // Only one deserialized.
+    extendableMessage.getExtension(SingularExtensions.someInt32);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedUint32);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedSint32);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedInt64);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedUint64);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedSint64);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedFixed32);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedSfixed32);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedFixed64);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedSfixed64);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedBool);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedFloat);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedDouble);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedEnum);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedString);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedBytes);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedMessage);
+    extendableMessage.getExtension(RepeatedExtensions.repeatedGroup);
+    assertEquals(extendableMessage, messageCopy);
+    assertEquals(extendableMessage.hashCode(), messageCopy.hashCode());
+
+    // Both deserialized.
+    messageCopy.getExtension(SingularExtensions.someInt32);
+    messageCopy.getExtension(RepeatedExtensions.repeatedUint32);
+    messageCopy.getExtension(RepeatedExtensions.repeatedSint32);
+    messageCopy.getExtension(RepeatedExtensions.repeatedInt64);
+    messageCopy.getExtension(RepeatedExtensions.repeatedUint64);
+    messageCopy.getExtension(RepeatedExtensions.repeatedSint64);
+    messageCopy.getExtension(RepeatedExtensions.repeatedFixed32);
+    messageCopy.getExtension(RepeatedExtensions.repeatedSfixed32);
+    messageCopy.getExtension(RepeatedExtensions.repeatedFixed64);
+    messageCopy.getExtension(RepeatedExtensions.repeatedSfixed64);
+    messageCopy.getExtension(RepeatedExtensions.repeatedBool);
+    messageCopy.getExtension(RepeatedExtensions.repeatedFloat);
+    messageCopy.getExtension(RepeatedExtensions.repeatedDouble);
+    messageCopy.getExtension(RepeatedExtensions.repeatedEnum);
+    messageCopy.getExtension(RepeatedExtensions.repeatedString);
+    messageCopy.getExtension(RepeatedExtensions.repeatedBytes);
+    messageCopy.getExtension(RepeatedExtensions.repeatedMessage);
+    messageCopy.getExtension(RepeatedExtensions.repeatedGroup);
+    assertEquals(extendableMessage, messageCopy);
+    assertEquals(extendableMessage.hashCode(), messageCopy.hashCode());
+
+    // Change one, make sure they are still different.
+    messageCopy.getExtension(RepeatedExtensions.repeatedMessage)[0].string = "not empty";
+    assertFalse(extendableMessage.equals(messageCopy));
+
+    // Even if the extension hasn't been deserialized.
+    extendableMessage = Extensions.ExtendableMessage.parseFrom(data);
+    assertFalse(extendableMessage.equals(messageCopy));
+  }
+
+  public void testExtensionsCaching() {
+    Extensions.ExtendableMessage extendableMessage = new Extensions.ExtendableMessage();
+    extendableMessage.setExtension(SingularExtensions.someMessage,
+        new Extensions.AnotherMessage());
+    assertSame("Consecutive calls to getExtensions should return the same object",
+        extendableMessage.getExtension(SingularExtensions.someMessage),
+        extendableMessage.getExtension(SingularExtensions.someMessage));
+  }
+
   public void testUnknownFields() throws Exception {
     // Check that we roundtrip (serialize and deserialize) unrecognized fields.
     AnotherMessage message = new AnotherMessage();