ICU-21010 MeasureUnit extension in Java

See #1275
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java
new file mode 100644
index 0000000..db98db0
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java
@@ -0,0 +1,737 @@
+// © 2020 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+
+package com.ibm.icu.impl.units;
+
+import com.ibm.icu.util.*;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+public class MeasureUnitImpl {
+
+    /**
+     * The full unit identifier.  Null if not computed.
+     */
+    private String identifier = null;
+
+    /**
+     * The complexity, either SINGLE, COMPOUND, or MIXED.
+     */
+    private MeasureUnit.Complexity complexity = MeasureUnit.Complexity.SINGLE;
+
+    /**
+     * The list of simple units. These may be summed or multiplied, based on the
+     * value of the complexity field.
+     * <p>
+     * The "dimensionless" unit (SingleUnitImpl default constructor) must not be
+     * added to this list.
+     * <p>
+     * The "dimensionless" <code>MeasureUnitImpl</code> has an empty <code>singleUnits</code>.
+     */
+    private ArrayList<SingleUnitImpl> singleUnits;
+
+    public MeasureUnitImpl() {
+        singleUnits = new ArrayList<>();
+    }
+
+    public MeasureUnitImpl(SingleUnitImpl singleUnit) {
+        this();
+        this.appendSingleUnit(singleUnit);
+    }
+
+    /**
+     * Parse a unit identifier into a MeasureUnitImpl.
+     *
+     * @param identifier The unit identifier string.
+     * @return A newly parsed object.
+     * @throws <code>IllegalArgumentException</code> in case of incorrect/non-parsed identifier.
+     */
+    public static MeasureUnitImpl forIdentifier(String identifier) {
+        return UnitsParser.parseForIdentifier(identifier);
+    }
+
+    /**
+     * Used for currency units.
+     */
+    public static MeasureUnitImpl forCurrencyCode(String currencyCode) {
+        MeasureUnitImpl result = new MeasureUnitImpl();
+        result.identifier = currencyCode;
+        return result;
+    }
+
+    public MeasureUnitImpl clone() {
+        MeasureUnitImpl result = new MeasureUnitImpl();
+        result.complexity = this.complexity;
+        result.identifier = this.identifier;
+        result.singleUnits = (ArrayList<SingleUnitImpl>) this.singleUnits.clone();
+        return result;
+    }
+
+    /**
+     * Returns the list of simple units.
+     */
+    public ArrayList<SingleUnitImpl> getSingleUnits() {
+        return singleUnits;
+    }
+
+    /**
+     * Mutates this MeasureUnitImpl to take the reciprocal.
+     */
+    public void takeReciprocal() {
+        this.identifier = null;
+        for (SingleUnitImpl singleUnit :
+                this.singleUnits) {
+            singleUnit.setDimensionality(singleUnit.getDimensionality() * -1);
+        }
+    }
+
+    /**
+     * Extracts the list of all the individual units inside the `MeasureUnitImpl`.
+     * For example:
+     * -   if the <code>MeasureUnitImpl</code> is <code>foot-per-hour</code>
+     * it will return a list of 1 <code>{foot-per-hour}</code>
+     * -   if the <code>MeasureUnitImpl</code> is <code>foot-and-inch</code>
+     * it will return a list of 2 <code>{ foot, inch}</code>
+     *
+     * @return a list of <code>MeasureUnitImpl</code>
+     */
+    public ArrayList<MeasureUnitImpl> extractIndividualUnits() {
+        ArrayList<MeasureUnitImpl> result = new ArrayList<MeasureUnitImpl>();
+        if (this.getComplexity() == MeasureUnit.Complexity.MIXED) {
+            // In case of mixed units, each single unit can be considered as a stand alone MeasureUnitImpl.
+            for (SingleUnitImpl singleUnit :
+                    this.getSingleUnits()) {
+                result.add(new MeasureUnitImpl(singleUnit));
+            }
+
+            return result;
+        }
+
+        result.add(this.clone());
+        return result;
+    }
+
+    /**
+     * Mutates this MeasureUnitImpl to append a single unit.
+     *
+     * @return true if a new item was added. If unit is the dimensionless unit,
+     * it is never added: the return value will always be false.
+     */
+    public boolean appendSingleUnit(SingleUnitImpl singleUnit) {
+        identifier = null;
+
+        if (singleUnit == null) {
+            // We don't append dimensionless units.
+            return false;
+        }
+
+        // Find a similar unit that already exists, to attempt to coalesce
+        SingleUnitImpl oldUnit = null;
+        for (int i = 0, n = this.singleUnits.size(); i < n; i++) {
+            SingleUnitImpl candidate = this.singleUnits.get(i);
+            if (candidate.isCompatibleWith(singleUnit)) {
+                oldUnit = candidate;
+                break;
+            }
+        }
+
+        if (oldUnit != null) {
+            // Both dimensionalities will be positive, or both will be negative, by
+            // virtue of isCompatibleWith().
+            oldUnit.setDimensionality(oldUnit.getDimensionality() + singleUnit.getDimensionality());
+
+            return false;
+        }
+
+        // Add a copy of singleUnit
+        this.singleUnits.add(singleUnit.clone());
+
+        // If the MeasureUnitImpl is `UMEASURE_UNIT_SINGLE` and after the appending a unit, the singleUnits are more
+        // than one singleUnit. thus means the complexity should be `UMEASURE_UNIT_COMPOUND`
+        if (this.singleUnits.size() > 1 && this.complexity == MeasureUnit.Complexity.SINGLE) {
+            this.setComplexity(MeasureUnit.Complexity.COMPOUND);
+        }
+
+        return true;
+    }
+
+    /**
+     * Transform this MeasureUnitImpl into a MeasureUnit, simplifying if possible.
+     * <p>
+     * NOTE: this function must be called from a thread-safe class
+     */
+    public MeasureUnit build() {
+        return MeasureUnit.fromMeasureUnitImpl(this);
+    }
+
+    /**
+     * @return SingleUnitImpl
+     * @throws UnsupportedOperationException if the object could not be converted to SingleUnitImpl.
+     */
+    public SingleUnitImpl getSingleUnitImpl() {
+        if (this.singleUnits.size() == 0) {
+            return new SingleUnitImpl();
+        }
+        if (this.singleUnits.size() == 1) {
+            return this.singleUnits.get(0).clone();
+        }
+
+        throw new UnsupportedOperationException();
+    }
+
+
+    /**
+     * Returns the CLDR unit identifier and null if not computed.
+     */
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public MeasureUnit.Complexity getComplexity() {
+        return complexity;
+    }
+
+    public void setComplexity(MeasureUnit.Complexity complexity) {
+        this.complexity = complexity;
+    }
+
+    /**
+     * Normalizes the MeasureUnitImpl and generates the identifier string in place.
+     */
+    public void serialize() {
+        if (this.getSingleUnits().size() == 0) {
+            // Dimensionless, constructed by the default constructor: no appending
+            // to this.result, we wish it to contain the zero-length string.
+            return;
+        }
+        if (this.complexity == MeasureUnit.Complexity.COMPOUND) {
+            // Note: don't sort a MIXED unit
+            Collections.sort(this.getSingleUnits(), new SingleUnitComparator());
+        }
+
+        StringBuilder result = new StringBuilder();
+        boolean beforePer = true;
+        boolean firstTimeNegativeDimension = false;
+        for (SingleUnitImpl singleUnit :
+                this.getSingleUnits()) {
+            if (beforePer && singleUnit.getDimensionality() < 0) {
+                beforePer = false;
+                firstTimeNegativeDimension = true;
+            } else if (singleUnit.getDimensionality() < 0) {
+                firstTimeNegativeDimension = false;
+            }
+
+            String singleUnitIdentifier = singleUnit.getNeutralIdentifier();
+            if (this.getComplexity() == MeasureUnit.Complexity.MIXED) {
+                if (result.length() != 0) {
+                    result.append("-and-");
+                }
+            } else {
+                if (firstTimeNegativeDimension) {
+                    if (result.length() == 0) {
+                        result.append("per-");
+                    } else {
+                        result.append("-per-");
+                    }
+                } else {
+                    if (result.length() != 0) {
+                        result.append("-");
+                    }
+                }
+            }
+
+            result.append(singleUnitIdentifier);
+        }
+
+        this.identifier = result.toString();
+    }
+
+    public enum CompoundPart {
+        // Represents "-per-"
+        PER(0),
+        // Represents "-"
+        TIMES(1),
+        // Represents "-and-"
+        AND(2);
+
+        private final int index;
+
+        CompoundPart(int index) {
+            this.index = index;
+        }
+
+        public static CompoundPart getCompoundPartFromTrieIndex(int trieIndex) {
+            int index = trieIndex - UnitsData.Constants.kCompoundPartOffset;
+            switch (index) {
+                case 0:
+                    return CompoundPart.PER;
+                case 1:
+                    return CompoundPart.TIMES;
+                case 2:
+                    return CompoundPart.AND;
+                default:
+                    throw new AssertionError("CompoundPart index must be 0, 1 or 2");
+            }
+        }
+
+        public int getTrieIndex() {
+            return this.index + UnitsData.Constants.kCompoundPartOffset;
+        }
+
+        public int getValue() {
+            return index;
+        }
+    }
+
+    public enum PowerPart {
+        P2(2),
+        P3(3),
+        P4(4),
+        P5(5),
+        P6(6),
+        P7(7),
+        P8(8),
+        P9(9),
+        P10(10),
+        P11(11),
+        P12(12),
+        P13(13),
+        P14(14),
+        P15(15);
+
+        private final int power;
+
+        PowerPart(int power) {
+            this.power = power;
+        }
+
+        public static int getPowerFromTrieIndex(int trieIndex) {
+            return trieIndex - UnitsData.Constants.kPowerPartOffset;
+        }
+
+        public int getTrieIndex() {
+            return this.power + UnitsData.Constants.kPowerPartOffset;
+        }
+
+        public int getValue() {
+            return power;
+        }
+    }
+
+    public enum InitialCompoundPart {
+
+        // Represents "per-", the only compound part that can appear at the start of
+        // an identifier.
+        INITIAL_COMPOUND_PART_PER(0);
+
+        private final int index;
+
+        InitialCompoundPart(int powerIndex) {
+            this.index = powerIndex;
+        }
+
+        public static InitialCompoundPart getInitialCompoundPartFromTrieIndex(int trieIndex) {
+            int index = trieIndex - UnitsData.Constants.kInitialCompoundPartOffset;
+            if (index == 0) {
+                return INITIAL_COMPOUND_PART_PER;
+            }
+
+            throw new IllegalArgumentException("Incorrect trieIndex");
+        }
+
+        public int getTrieIndex() {
+            return this.index + UnitsData.Constants.kInitialCompoundPartOffset;
+        }
+
+        public int getValue() {
+            return index;
+        }
+
+    }
+
+    public static class UnitsParser {
+        // This used only to not build the trie each time we use the parser
+        private volatile static CharsTrie savedTrie = null;
+        private final String[] simpleUnits;
+        // This trie used in the parsing operation.
+        private CharsTrie trie;
+        // Tracks parser progress: the offset into fSource.
+        private int fIndex = 0;
+        // Set to true when we've seen a "-per-" or a "per-", after which all units
+        // are in the denominator. Until we find an "-and-", at which point the
+        // identifier is invalid pending TODO(CLDR-13700).
+        private boolean fAfterPer = false;
+        private String fSource;
+        // If an "-and-" was parsed prior to finding the "single
+        //     * unit", sawAnd is set to true. If not, it is left as is.
+        private boolean fSawAnd = false;
+
+        private UnitsParser(String identifier) {
+            this.simpleUnits = UnitsData.getSimpleUnits();
+            this.fSource = identifier;
+
+            if (UnitsParser.savedTrie != null) {
+                try {
+                    this.trie = UnitsParser.savedTrie.clone();
+                } catch (CloneNotSupportedException e) {
+                    throw new ICUCloneNotSupportedException();
+                }
+                return;
+            }
+
+            // Building the trie.
+            CharsTrieBuilder trieBuilder;
+            trieBuilder = new CharsTrieBuilder();
+
+            // Add syntax parts (compound, power prefixes)
+            trieBuilder.add("-per-", CompoundPart.PER.getTrieIndex());
+            trieBuilder.add("-", CompoundPart.TIMES.getTrieIndex());
+            trieBuilder.add("-and-", CompoundPart.AND.getTrieIndex());
+            trieBuilder.add("per-", InitialCompoundPart.INITIAL_COMPOUND_PART_PER.getTrieIndex());
+            trieBuilder.add("square-", PowerPart.P2.getTrieIndex());
+            trieBuilder.add("cubic-", PowerPart.P3.getTrieIndex());
+            trieBuilder.add("pow2-", PowerPart.P2.getTrieIndex());
+            trieBuilder.add("pow3-", PowerPart.P3.getTrieIndex());
+            trieBuilder.add("pow4-", PowerPart.P4.getTrieIndex());
+            trieBuilder.add("pow5-", PowerPart.P5.getTrieIndex());
+            trieBuilder.add("pow6-", PowerPart.P6.getTrieIndex());
+            trieBuilder.add("pow7-", PowerPart.P7.getTrieIndex());
+            trieBuilder.add("pow8-", PowerPart.P8.getTrieIndex());
+            trieBuilder.add("pow9-", PowerPart.P9.getTrieIndex());
+            trieBuilder.add("pow10-", PowerPart.P10.getTrieIndex());
+            trieBuilder.add("pow11-", PowerPart.P11.getTrieIndex());
+            trieBuilder.add("pow12-", PowerPart.P12.getTrieIndex());
+            trieBuilder.add("pow13-", PowerPart.P13.getTrieIndex());
+            trieBuilder.add("pow14-", PowerPart.P14.getTrieIndex());
+            trieBuilder.add("pow15-", PowerPart.P15.getTrieIndex());
+
+            // Add SI prefixes
+            for (MeasureUnit.SIPrefix siPrefix :
+                    MeasureUnit.SIPrefix.values()) {
+                trieBuilder.add(siPrefix.getIdentifier(), getTrieIndex(siPrefix));
+            }
+
+            // Add simple units
+            for (int i = 0; i < simpleUnits.length; i++) {
+                trieBuilder.add(simpleUnits[i], i + UnitsData.Constants.kSimpleUnitOffset);
+
+            }
+
+            // TODO: Use SLOW or FAST here?
+            UnitsParser.savedTrie = trieBuilder.build(StringTrieBuilder.Option.FAST);
+
+            try {
+                this.trie = UnitsParser.savedTrie.clone();
+            } catch (CloneNotSupportedException e) {
+                throw new ICUCloneNotSupportedException();
+            }
+        }
+
+
+        /**
+         * Construct a MeasureUnit from a CLDR Unit Identifier, defined in UTS 35.
+         * Validates and canonicalizes the identifier.
+         *
+         * @return MeasureUnitImpl object or null if the identifier is empty.
+         * @throws IllegalArgumentException in case of invalid identifier.
+         */
+        public static MeasureUnitImpl parseForIdentifier(String identifier) {
+            if (identifier == null || identifier.isEmpty()) {
+                return null;
+            }
+
+            UnitsParser parser = new UnitsParser(identifier);
+            return parser.parse();
+
+        }
+
+        private static MeasureUnit.SIPrefix getSiPrefixFromTrieIndex(int trieIndex) {
+            for (MeasureUnit.SIPrefix element :
+                    MeasureUnit.SIPrefix.values()) {
+                if (getTrieIndex(element) == trieIndex)
+                    return element;
+            }
+
+            throw new IllegalArgumentException("Incorrect trieIndex");
+        }
+
+        private static int getTrieIndex(MeasureUnit.SIPrefix prefix) {
+            return prefix.getSiPrefixPower() + UnitsData.Constants.kSIPrefixOffset;
+        }
+
+        private MeasureUnitImpl parse() {
+            MeasureUnitImpl result = new MeasureUnitImpl();
+
+            if (fSource.isEmpty()) {
+                // The dimensionless unit: nothing to parse. return null.
+                return null;
+            }
+
+            while (hasNext()) {
+                fSawAnd = false;
+                SingleUnitImpl singleUnit = nextSingleUnit();
+
+                boolean added = result.appendSingleUnit(singleUnit);
+                if (fSawAnd && !added) {
+                    throw new IllegalArgumentException("Two similar units are not allowed in a mixed unit.");
+                }
+
+                if ((result.singleUnits.size()) >= 2) {
+                    // nextSingleUnit fails appropriately for "per" and "and" in the
+                    // same identifier. It doesn't fail for other compound units
+                    // (COMPOUND_PART_TIMES). Consequently we take care of that
+                    // here.
+                    MeasureUnit.Complexity complexity =
+                            fSawAnd ? MeasureUnit.Complexity.MIXED : MeasureUnit.Complexity.COMPOUND;
+                    if (result.getSingleUnits().size() == 2) {
+                        // After appending two singleUnits, the complexity will be `UMEASURE_UNIT_COMPOUND`
+                        assert result.getComplexity() == MeasureUnit.Complexity.COMPOUND;
+                        result.setComplexity(complexity);
+                    } else if (result.getComplexity() != complexity) {
+                        throw new IllegalArgumentException("Can't have mixed compound units");
+                    }
+                }
+            }
+
+            return result;
+        }
+
+        /**
+         * Returns the next "single unit" via result.
+         * <p>
+         * If a "-per-" was parsed, the result will have appropriate negative
+         * dimensionality.
+         * <p>
+         *
+         * @throws IllegalArgumentException if we parse both compound units and "-and-", since mixed
+         *                                  compound units are not yet supported - TODO(CLDR-13700).
+         */
+        private SingleUnitImpl nextSingleUnit() {
+            SingleUnitImpl result = new SingleUnitImpl();
+
+            // state:
+            // 0 = no tokens seen yet (will accept power, SI prefix, or simple unit)
+            // 1 = power token seen (will not accept another power token)
+            // 2 = SI prefix token seen (will not accept a power or SI prefix token)
+            int state = 0;
+
+            boolean atStart = fIndex == 0;
+            Token token = nextToken();
+
+            if (atStart) {
+                // Identifiers optionally start with "per-".
+                if (token.getType() == Token.Type.TYPE_INITIAL_COMPOUND_PART) {
+                    assert token.getInitialCompoundPart() == InitialCompoundPart.INITIAL_COMPOUND_PART_PER;
+
+                    fAfterPer = true;
+                    result.setDimensionality(-1);
+
+                    token = nextToken();
+                }
+            } else {
+                // All other SingleUnit's are separated from previous SingleUnit's
+                // via a compound part:
+                if (token.getType() != Token.Type.TYPE_COMPOUND_PART) {
+                    throw new IllegalArgumentException("token type must be TYPE_COMPOUND_PART");
+                }
+
+                CompoundPart compoundPart = CompoundPart.getCompoundPartFromTrieIndex(token.getMatch());
+                switch (compoundPart) {
+                    case PER:
+                        if (fSawAnd) {
+                            throw new IllegalArgumentException("Mixed compound units not yet supported");
+                            // TODO(CLDR-13700).
+                        }
+
+                        fAfterPer = true;
+                        result.setDimensionality(-1);
+                        break;
+
+                    case TIMES:
+                        if (fAfterPer) {
+                            result.setDimensionality(-1);
+                        }
+                        break;
+
+                    case AND:
+                        if (fAfterPer) {
+                            // not yet supported, TODO(CLDR-13700).
+                            throw new IllegalArgumentException("Can't start with \"-and-\", and mixed compound units");
+                        }
+                        fSawAnd = true;
+                        break;
+                }
+
+                token = nextToken();
+            }
+
+            // Read tokens until we have a complete SingleUnit or we reach the end.
+            while (true) {
+                switch (token.getType()) {
+                    case TYPE_POWER_PART:
+                        if (state > 0) {
+                            throw new IllegalArgumentException();
+                        }
+
+                        result.setDimensionality(result.getDimensionality() * token.getPower());
+                        state = 1;
+                        break;
+
+                    case TYPE_SI_PREFIX:
+                        if (state > 1) {
+                            throw new IllegalArgumentException();
+                        }
+
+                        result.setSiPrefix(token.getSIPrefix());
+                        state = 2;
+                        break;
+
+                    case TYPE_SIMPLE_UNIT:
+                        result.setSimpleUnit(token.getSimpleUnitIndex(), simpleUnits);
+                        return result;
+
+                    default:
+                        throw new IllegalArgumentException();
+                }
+
+                if (!hasNext()) {
+                    throw new IllegalArgumentException("We ran out of tokens before finding a complete single unit.");
+                }
+
+                token = nextToken();
+            }
+        }
+
+        private boolean hasNext() {
+            return fIndex < fSource.length();
+        }
+
+        private Token nextToken() {
+            trie.reset();
+            int match = -1;
+            // Saves the position in the fSource string for the end of the most
+            // recent matching token.
+            int previ = -1;
+
+            // Find the longest token that matches a value in the trie:
+            while (fIndex < fSource.length()) {
+                BytesTrie.Result result = trie.next(fSource.charAt(fIndex++));
+                if (result == BytesTrie.Result.NO_MATCH) {
+                    break;
+                } else if (result == BytesTrie.Result.NO_VALUE) {
+                    continue;
+                }
+
+                match = trie.getValue();
+                previ = fIndex;
+
+                if (result == BytesTrie.Result.FINAL_VALUE) {
+                    break;
+                }
+
+                if (result != BytesTrie.Result.INTERMEDIATE_VALUE) {
+                    throw new IllegalArgumentException("result must has an intermediate value");
+                }
+
+                // continue;
+            }
+
+
+            if (match < 0) {
+                throw new IllegalArgumentException("Encountered unknown token starting at index " + previ);
+            } else {
+                fIndex = previ;
+            }
+
+            return new Token(match);
+        }
+
+        static class Token {
+
+            private final int fMatch;
+            private final Type type;
+
+            public Token(int fMatch) {
+                this.fMatch = fMatch;
+                type = calculateType(fMatch);
+            }
+
+            public Type getType() {
+                return this.type;
+            }
+
+            public MeasureUnit.SIPrefix getSIPrefix() {
+                assert this.type == Type.TYPE_SI_PREFIX;
+                return getSiPrefixFromTrieIndex(this.fMatch);
+            }
+
+            // Valid only for tokens with type TYPE_COMPOUND_PART.
+            public int getMatch() {
+                assert getType() == Type.TYPE_COMPOUND_PART;
+                return fMatch;
+            }
+
+            // Even if there is only one InitialCompoundPart value, we have this
+            // function for the simplicity of code consistency.
+            public InitialCompoundPart getInitialCompoundPart() {
+                assert (this.type == Type.TYPE_INITIAL_COMPOUND_PART
+                        &&
+                        fMatch == InitialCompoundPart.INITIAL_COMPOUND_PART_PER.getTrieIndex());
+                return InitialCompoundPart.getInitialCompoundPartFromTrieIndex(fMatch);
+            }
+
+            public int getPower() {
+                assert this.type == Type.TYPE_POWER_PART;
+                return PowerPart.getPowerFromTrieIndex(this.fMatch);
+            }
+
+            public int getSimpleUnitIndex() {
+                return this.fMatch - UnitsData.Constants.kSimpleUnitOffset;
+            }
+
+            // Calling calculateType() is invalid, resulting in an assertion failure, if Token
+            // value isn't positive.
+            private Type calculateType(int fMatch) {
+                if (fMatch <= 0) {
+                    throw new AssertionError("fMatch must have a positive value");
+                }
+
+                if (fMatch < UnitsData.Constants.kCompoundPartOffset) {
+                    return Type.TYPE_SI_PREFIX;
+                }
+                if (fMatch < UnitsData.Constants.kInitialCompoundPartOffset) {
+                    return Type.TYPE_COMPOUND_PART;
+                }
+                if (fMatch < UnitsData.Constants.kPowerPartOffset) {
+                    return Type.TYPE_INITIAL_COMPOUND_PART;
+                }
+                if (fMatch < UnitsData.Constants.kSimpleUnitOffset) {
+                    return Type.TYPE_POWER_PART;
+                }
+
+                return Type.TYPE_SIMPLE_UNIT;
+            }
+
+            enum Type {
+                TYPE_UNDEFINED,
+                TYPE_SI_PREFIX,
+                // Token type for "-per-", "-", and "-and-".
+                TYPE_COMPOUND_PART,
+                // Token type for "per-".
+                TYPE_INITIAL_COMPOUND_PART,
+                TYPE_POWER_PART,
+                TYPE_SIMPLE_UNIT,
+            }
+        }
+    }
+
+    class SingleUnitComparator implements Comparator<SingleUnitImpl> {
+        @Override
+        public int compare(SingleUnitImpl o1, SingleUnitImpl o2) {
+            return o1.compareTo(o2);
+        }
+    }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/SingleUnitImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/SingleUnitImpl.java
new file mode 100644
index 0000000..52595c0
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/SingleUnitImpl.java
@@ -0,0 +1,157 @@
+// © 2020 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+
+package com.ibm.icu.impl.units;
+
+import com.ibm.icu.util.MeasureUnit;
+
+public class SingleUnitImpl {
+    /**
+     * Simple unit index, unique for every simple unit, -1 for the dimensionless
+     * unit. This is an index into a string list in unit.txt {ConversionUnits}.
+     * <p>
+     * The default value is -1, meaning the dimensionless unit:
+     * isDimensionless() will return true, until index is changed.
+     */
+    private int index = -1;
+    /**
+     * SimpleUnit is the simplest form of a Unit. For example, for "square-millimeter", the simple unit would be "meter"Ò
+     * <p>
+     * The default value is "", meaning the dimensionless unit:
+     * isDimensionless() will return true, until index is changed.
+     */
+    private String simpleUnit = "";
+    /**
+     * Determine the power of the `SingleUnit`. For example, for "square-meter", the dimensionality will be `2`.
+     * <p>
+     * NOTE:
+     * Default dimensionality is 1.
+     */
+    private int dimensionality = 1;
+    /**
+     * SI Prefix
+     */
+    private MeasureUnit.SIPrefix siPrefix = MeasureUnit.SIPrefix.ONE;
+
+    public SingleUnitImpl clone() {
+        SingleUnitImpl result = new SingleUnitImpl();
+        result.index = this.index;
+        result.dimensionality = this.dimensionality;
+        result.simpleUnit = this.simpleUnit;
+        result.siPrefix = this.siPrefix;
+
+        return result;
+    }
+
+    public MeasureUnit build() {
+        MeasureUnitImpl measureUnit = new MeasureUnitImpl(this);
+        return measureUnit.build();
+    }
+
+    /**
+     * Generates an neutral identifier string for a single unit which means we do not include the dimension signal.
+     *
+     * @throws IllegalArgumentException
+     */
+    public String getNeutralIdentifier() {
+        StringBuilder result = new StringBuilder();
+        int posPower = Math.abs(this.getDimensionality());
+
+        assert posPower > 0 : "getIdentifier does not support the dimensionless";
+
+        if (posPower == 1) {
+            // no-op
+        } else if (posPower == 2) {
+            result.append("square-");
+        } else if (posPower == 3) {
+            result.append("cubic-");
+        } else if (posPower <= 15) {
+            result.append("pow");
+            result.append(posPower);
+            result.append('-');
+        } else {
+            throw new IllegalArgumentException("Unit Identifier Syntax Error");
+        }
+
+        result.append(this.getSiPrefix().getIdentifier());
+        result.append(this.getSimpleUnit());
+
+        return result.toString();
+    }
+
+    /**
+     * Compare this SingleUnitImpl to another SingleUnitImpl for the sake of
+     * sorting and coalescing.
+     * <p>
+     * Takes the sign of dimensionality into account, but not the absolute
+     * value: per-meter is not considered the same as meter, but meter is
+     * considered the same as square-meter.
+     * <p>
+     * The dimensionless unit generally does not get compared, but if it did, it
+     * would sort before other units by virtue of index being < 0 and
+     * dimensionality not being negative.
+     */
+    int compareTo(SingleUnitImpl other) {
+        if (dimensionality < 0 && other.dimensionality > 0) {
+            // Positive dimensions first
+            return 1;
+        }
+        if (dimensionality > 0 && other.dimensionality < 0) {
+            return -1;
+        }
+        if (index < other.index) {
+            return -1;
+        }
+        if (index > other.index) {
+            return 1;
+        }
+        if (this.getSiPrefix().getSiPrefixPower() < other.getSiPrefix().getSiPrefixPower()) {
+            return -1;
+        }
+        if (this.getSiPrefix().getSiPrefixPower() > other.getSiPrefix().getSiPrefixPower()) {
+            return 1;
+        }
+        return 0;
+    }
+
+    /**
+     * Checks whether this SingleUnitImpl is compatible with another for the purpose of coalescing.
+     * <p>
+     * Units with the same base unit and SI prefix should match, except that they must also have
+     * the same dimensionality sign, such that we don't merge numerator and denominator.
+     */
+    boolean isCompatibleWith(SingleUnitImpl other) {
+        return (compareTo(other) == 0);
+    }
+
+    public String getSimpleUnit() {
+        return simpleUnit;
+    }
+
+    public void setSimpleUnit(int simpleUnitIndex, String[] simpleUnits) {
+        this.index = simpleUnitIndex;
+        this.simpleUnit = simpleUnits[simpleUnitIndex];
+    }
+
+    public int getDimensionality() {
+        return dimensionality;
+    }
+
+    public void setDimensionality(int dimensionality) {
+        this.dimensionality = dimensionality;
+    }
+
+    public MeasureUnit.SIPrefix getSiPrefix() {
+        return siPrefix;
+    }
+
+    public void setSiPrefix(MeasureUnit.SIPrefix siPrefix) {
+        this.siPrefix = siPrefix;
+    }
+
+    public int getIndex() {
+        return index;
+    }
+
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsData.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsData.java
new file mode 100644
index 0000000..87b4e20
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsData.java
@@ -0,0 +1,92 @@
+// © 2020 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+
+package com.ibm.icu.impl.units;
+
+import com.ibm.icu.impl.ICUData;
+import com.ibm.icu.impl.ICUResourceBundle;
+import com.ibm.icu.impl.UResource;
+import com.ibm.icu.util.UResourceBundle;
+
+import java.util.ArrayList;
+
+/**
+ * Responsible for all units data operations (retriever, analysis, extraction certain data ... etc.).
+ */
+class UnitsData {
+    private volatile static String[] simpleUnits = null;
+
+    public static String[] getSimpleUnits() {
+        if (simpleUnits != null) {
+            return simpleUnits;
+        }
+
+        // Read simple units
+        ICUResourceBundle resource;
+        resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "units");
+        SimpleUnitIdentifiersSink sink = new SimpleUnitIdentifiersSink();
+        resource.getAllItemsWithFallback("convertUnits", sink);
+        simpleUnits = sink.simpleUnits;
+
+        return simpleUnits;
+    }
+
+    public static class SimpleUnitIdentifiersSink extends UResource.Sink {
+        String[] simpleUnits = null;
+
+        @Override
+        public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
+            assert key.toString().equals(Constants.CONVERSION_UNIT_TABLE_NAME);
+            assert value.getType() == UResourceBundle.TABLE;
+
+            UResource.Table simpleUnitsTable = value.getTable();
+            ArrayList<String> simpleUnits = new ArrayList<>();
+            for (int i = 0; simpleUnitsTable.getKeyAndValue(i, key, value); i++) {
+                if (key.toString().equals("kilogram")) {
+
+                    // For parsing, we use "gram", the prefixless metric mass unit. We
+                    // thus ignore the SI Base Unit of Mass: it exists due to being the
+                    // mass conversion target unit, but not needed for MeasureUnit
+                    // parsing.
+                    continue;
+                }
+
+                simpleUnits.add(key.toString());
+            }
+
+            this.simpleUnits = simpleUnits.toArray(new String[0]);
+        }
+    }
+
+    /**
+     * Contains all the needed constants.
+     */
+    public static class Constants {
+        // Trie value offset for simple units, e.g. "gram", "nautical-mile",
+        // "fluid-ounce-imperial".
+        public static final int kSimpleUnitOffset = 512;
+
+        // Trie value offset for powers like "square-", "cubic-", "pow2-" etc.
+        public static final int kPowerPartOffset = 256;
+
+
+        // Trie value offset for "per-".
+        public final static int kInitialCompoundPartOffset = 192;
+
+        // Trie value offset for compound parts, e.g. "-per-", "-", "-and-".
+        public final static int kCompoundPartOffset = 128;
+
+        // Trie value offset for SI Prefixes. This is big enough to ensure we only
+        // insert positive integers into the trie.
+        public static final int kSIPrefixOffset = 64;
+
+
+        /* Tables Names*/
+        public static final String CONVERSION_UNIT_TABLE_NAME = "convertUnits";
+        public static final String UNIT_PREFERENCE_TABLE_NAME = "unitPreferenceData";
+        public static final String CATEGORY_TABLE_NAME = "unitQuantities";
+        public static final String DEFAULT_REGION = "001";
+        public static final String DEFAULT_USAGE = "default";
+    }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java b/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java
index 7b9334a..32c05c4 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java
@@ -14,10 +14,12 @@
 import java.io.ObjectOutput;
 import java.io.ObjectStreamException;
 import java.io.Serializable;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
+import java.util.Map;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Map;
 import java.util.Set;
 
 import com.ibm.icu.impl.CollectionSet;
@@ -25,8 +27,12 @@
 import com.ibm.icu.impl.ICUResourceBundle;
 import com.ibm.icu.impl.Pair;
 import com.ibm.icu.impl.UResource;
+import com.ibm.icu.impl.units.MeasureUnitImpl;
+import com.ibm.icu.impl.units.SingleUnitImpl;
 import com.ibm.icu.text.UnicodeSet;
 
+
+
 /**
  * A unit such as length, mass, volume, currency, etc.  A unit is
  * coupled with a numeric amount to produce a Measure. MeasureUnit objects are immutable.
@@ -48,6 +54,7 @@
     private static boolean cacheIsPopulated = false;
 
     /**
+     * If type set to null, measureUnitImpl is in use instead of type and subType.
      * @internal
      * @deprecated This API is ICU internal only.
      */
@@ -55,6 +62,7 @@
     protected final String type;
 
     /**
+     * If subType set to null, measureUnitImpl is in use instead of type and subType.
      * @internal
      * @deprecated This API is ICU internal only.
      */
@@ -62,6 +70,248 @@
     protected final String subType;
 
     /**
+     * Used by new draft APIs in ICU 68.
+     *
+     * @internal
+     */
+    private MeasureUnitImpl measureUnitImpl = null;
+
+    /**
+     * Enumeration for unit complexity. There are three levels:
+     * <p>
+     * - SINGLE: A single unit, optionally with a power and/or SI prefix. Examples: hectare,
+     * square-kilometer, kilojoule, one-per-second.
+     * - COMPOUND: A unit composed of the product of multiple single units. Examples:
+     * meter-per-second, kilowatt-hour, kilogram-meter-per-square-second.
+     * - MIXED: A unit composed of the sum of multiple single units. Examples: foot-and-inch,
+     * hour-and-minute-and-second, degree-and-arcminute-and-arcsecond.
+     * <p>
+     * The complexity determines which operations are available. For example, you cannot set the power
+     * or SI prefix of a compound unit.
+     *
+     * @draft ICU 68
+     * @provisional This API might change or be removed in a future release.
+     */
+    public enum Complexity {
+        /**
+         * A single unit, like kilojoule.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        SINGLE,
+
+        /**
+         * A compound unit, like meter-per-second.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        COMPOUND,
+
+        /**
+         * A mixed unit, like hour-and-minute.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        MIXED
+    }
+
+    /**
+     * Enumeration for SI prefixes, such as "kilo".
+     *
+     * @draft ICU 68
+     * @provisional This API might change or be removed in a future release.
+     */
+    public enum SIPrefix {
+
+        /**
+         * SI prefix: yotta, 10^24.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        YOTTA(24, "yotta"),
+
+        /**
+         * SI prefix: zetta, 10^21.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        ZETTA(21, "zetta"),
+
+        /**
+         * SI prefix: exa, 10^18.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        EXA(18, "exa"),
+
+        /**
+         * SI prefix: peta, 10^15.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        PETA(15, "peta"),
+
+        /**
+         * SI prefix: tera, 10^12.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        TERA(12, "tera"),
+
+        /**
+         * SI prefix: giga, 10^9.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        GIGA(9, "giga"),
+
+        /**
+         * SI prefix: mega, 10^6.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        MEGA(6, "mega"),
+
+        /**
+         * SI prefix: kilo, 10^3.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        KILO(3, "kilo"),
+
+        /**
+         * SI prefix: hecto, 10^2.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        HECTO(2, "hecto"),
+
+        /**
+         * SI prefix: deka, 10^1.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        DEKA(1, "deka"),
+
+        /**
+         * The absence of an SI prefix.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        ONE(0, ""),
+
+        /**
+         * SI prefix: deci, 10^-1.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        DECI(-1, "deci"),
+
+        /**
+         * SI prefix: centi, 10^-2.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        CENTI(-2, "centi"),
+
+        /**
+         * SI prefix: milli, 10^-3.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        MILLI(-3, "milli"),
+
+        /**
+         * SI prefix: micro, 10^-6.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        MICRO(-6, "micro"),
+
+        /**
+         * SI prefix: nano, 10^-9.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        NANO(-9, "nano"),
+
+        /**
+         * SI prefix: pico, 10^-12.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        PICO(-12, "pico"),
+
+        /**
+         * SI prefix: femto, 10^-15.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        FEMTO(-15, "femto"),
+
+        /**
+         * SI prefix: atto, 10^-18.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        ATTO(-18, "atto"),
+
+        /**
+         * SI prefix: zepto, 10^-21.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        ZEPTO(-21, "zepto"),
+
+        /**
+         * SI prefix: yocto, 10^-24.
+         *
+         * @draft ICU 68
+         * @provisional This API might change or be removed in a future release.
+         */
+        YOCTO(-24, "yocto");
+
+        private final int siPrefixPower;
+        private final String identifier;
+
+        SIPrefix(int siPrefixPower, String identifier) {
+            this.siPrefixPower = siPrefixPower;
+            this.identifier = identifier;
+        }
+
+        public String getIdentifier() {
+            return identifier;
+        }
+
+        public int getSiPrefixPower() {
+            return siPrefixPower;
+        }
+    }
+
+    /**
      * @internal
      * @deprecated This API is ICU internal only.
      */
@@ -72,6 +322,52 @@
     }
 
     /**
+     * Construct a MeasureUnit from a CLDR Unit Identifier, defined in UTS 35.
+     * Validates and canonicalizes the identifier.
+     *
+     * Note: dimensionless <code>MeasureUnit</code> is <code>null</code>
+     *
+     * <pre>
+     * MeasureUnit example = MeasureUnit::forIdentifier("furlong-per-nanosecond")
+     * </pre>
+     *
+     * @param identifier The CLDR Sequence Unit Identifier
+     * @throws IllegalArgumentException if the identifier is invalid.
+     * @draft ICU 68
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static MeasureUnit forIdentifier(String identifier) {
+        if (identifier == null || identifier.isEmpty()) {
+            return NoUnit.BASE;
+        }
+
+        return MeasureUnitImpl.forIdentifier(identifier).build();
+    }
+
+    /**
+     * @internal
+     * @param measureUnitImpl
+     */
+    public static MeasureUnit fromMeasureUnitImpl(MeasureUnitImpl measureUnitImpl) {
+        measureUnitImpl.serialize();
+        String identifier = measureUnitImpl.getIdentifier();
+        MeasureUnit result = MeasureUnit.findBySubType(identifier);
+        if (result != null) {
+            return result;
+        }
+
+        return new MeasureUnit(measureUnitImpl);
+    }
+
+    private MeasureUnit(MeasureUnitImpl measureUnitImpl) {
+        type = null;
+        subType = null;
+        this.measureUnitImpl = measureUnitImpl.clone();
+    }
+
+
+
+    /**
      * Get the type, such as "length"
      *
      * @stable ICU 53
@@ -90,7 +386,187 @@
         return subType;
     }
 
+    /**
+     * Gets the CLDR Unit Identifier for this MeasureUnit, as defined in UTS 35.
+     *
+     * @return The string form of this unit.
+     * @draft ICU 68
+     * @provisional This API might change or be removed in a future release.
+     */
+    public String getIdentifier() {
+        String result = measureUnitImpl == null ? getSubtype() : measureUnitImpl.getIdentifier();
+        return result == null ? "" : result;
+    }
 
+    /**
+     * Compute the complexity of the unit. See Complexity for more information.
+     *
+     * @return The unit complexity.
+     * @draft ICU 68
+     * @provisional This API might change or be removed in a future release.
+     */
+    public Complexity getComplexity() {
+        if (measureUnitImpl == null) {
+            return MeasureUnitImpl.forIdentifier(getIdentifier()).getComplexity();
+        }
+
+        return measureUnitImpl.getComplexity();
+    }
+
+    /**
+     * Creates a MeasureUnit which is this SINGLE unit augmented with the specified SI prefix.
+     * For example, SI_PREFIX_KILO for "kilo".
+     * May return this if this unit already has that prefix.
+     * <p>
+     * There is sufficient locale data to format all standard SI prefixes.
+     * <p>
+     * NOTE: Only works on SINGLE units. If this is a COMPOUND or MIXED unit, an error will
+     * occur. For more information, see `Complexity`.
+     *
+     * @param prefix The SI prefix, from SIPrefix.
+     * @return A new SINGLE unit.
+     * @throws UnsupportedOperationException if this unit is a COMPOUND or MIXED unit.
+     * @draft ICU 68
+     * @provisional This API might change or be removed in a future release.
+     */
+    public MeasureUnit withSIPrefix(SIPrefix prefix) {
+        SingleUnitImpl singleUnit = getSingleUnitImpl();
+        singleUnit.setSiPrefix(prefix);
+        return singleUnit.build();
+    }
+
+    /**
+     * Returns the current SI prefix of this SINGLE unit. For example, if the unit has the SI prefix
+     * "kilo", then SI_PREFIX_KILO is returned.
+     * <p>
+     * NOTE: Only works on SINGLE units. If this is a COMPOUND or MIXED unit, an error will
+     * occur. For more information, see `Complexity`.
+     *
+     * @return The SI prefix of this SINGLE unit, from SIPrefix.
+     * @throws UnsupportedOperationException if the unit is COMPOUND or MIXED.
+     * @draft ICU 68
+     * @provisional This API might change or be removed in a future release.
+     */
+    public SIPrefix getSIPrefix() {
+        return getSingleUnitImpl().getSiPrefix();
+    }
+
+    /**
+     * Returns the dimensionality (power) of this MeasureUnit. For example, if the unit is square,
+     * then 2 is returned.
+     * <p>
+     * NOTE: Only works on SINGLE units. If this is a COMPOUND or MIXED unit, an exception will be thrown.
+     * For more information, see `Complexity`.
+     *
+     * @return The dimensionality (power) of this simple unit.
+     * @throws UnsupportedOperationException if the unit is COMPOUND or MIXED.
+     * @draft ICU 68
+     * @provisional This API might change or be removed in a future release.
+     */
+    public int getDimensionality() {
+        return getSingleUnitImpl().getDimensionality();
+    }
+
+    /**
+     * Creates a MeasureUnit which is this SINGLE unit augmented with the specified dimensionality
+     * (power). For example, if dimensionality is 2, the unit will be squared.
+     * <p>
+     * NOTE: Only works on SINGLE units. If this is a COMPOUND or MIXED unit, an exception is thrown.
+     * For more information, see `Complexity`.
+     *
+     * @param dimensionality The dimensionality (power).
+     * @return A new SINGLE unit.
+     * @throws UnsupportedOperationException if the unit is COMPOUND or MIXED.
+     * @draft ICU 68
+     * @provisional This API might change or be removed in a future release.
+     */
+    public MeasureUnit withDimensionality(int dimensionality) {
+        SingleUnitImpl singleUnit = getSingleUnitImpl();
+        singleUnit.setDimensionality(dimensionality);
+        return singleUnit.build();
+    }
+
+    /**
+     * Computes the reciprocal of this MeasureUnit, with the numerator and denominator flipped.
+     * <p>
+     * For example, if the receiver is "meter-per-second", the unit "second-per-meter" is returned.
+     * <p>
+     * NOTE: Only works on SINGLE and COMPOUND units. If this is a MIXED unit, an error will
+     * occur. For more information, see `Complexity`.
+     *
+     * @return The reciprocal of the target unit.
+     * @throws UnsupportedOperationException if the unit is MIXED.
+     * @draft ICU 68
+     * @provisional This API might change or be removed in a future release.
+     */
+    public MeasureUnit reciprocal() {
+        MeasureUnitImpl measureUnit = getCopyOfMeasureUnitImpl();
+        measureUnit.takeReciprocal();
+        return measureUnit.build();
+    }
+
+    /**
+     * Computes the product of this unit with another unit. This is a way to build units from
+     * constituent parts.
+     * <p>
+     * The numerator and denominator are preserved through this operation.
+     * <p>
+     * For example, if the receiver is "kilowatt" and the argument is "hour-per-day", then the
+     * unit "kilowatt-hour-per-day" is returned.
+     * <p>
+     * NOTE: Only works on SINGLE and COMPOUND units. If either unit (receivee and argument) is a
+     * MIXED unit, an error will occur. For more information, see `Complexity`.
+     *
+     * @param other The MeasureUnit to multiply with the target.
+     * @return The product of the target unit with the provided unit.
+     * @throws UnsupportedOperationException if the unit is MIXED.
+     * @draft ICU 68
+     * @provisional This API might change or be removed in a future release.
+     */
+    public MeasureUnit product(MeasureUnit other) {
+        MeasureUnitImpl implCopy = getCopyOfMeasureUnitImpl();
+
+        if (other == null /* dimensionless */) {
+            return implCopy.build();
+        }
+
+        final MeasureUnitImpl otherImplRef = other.getMayBeReferenceOfMeasureUnitImpl();
+        if (implCopy.getComplexity() == Complexity.MIXED || otherImplRef.getComplexity() == Complexity.MIXED) {
+            throw new UnsupportedOperationException();
+        }
+
+        for (SingleUnitImpl singleUnit :
+                otherImplRef.getSingleUnits()) {
+            implCopy.appendSingleUnit(singleUnit);
+        }
+
+        return implCopy.build();
+    }
+
+    /**
+     * Returns the list of SINGLE units contained within a sequence of COMPOUND units.
+     * <p>
+     * Examples:
+     * - Given "meter-kilogram-per-second", three units will be returned: "meter",
+     * "kilogram", and "one-per-second".
+     * - Given "hour+minute+second", three units will be returned: "hour", "minute",
+     * and "second".
+     * <p>
+     * If this is a SINGLE unit, a list of length 1 will be returned.
+     *
+     * @return An unmodifiable list of single units
+     * @internal ICU 68 Technology Preview
+     * @provisional This API might change or be removed in a future release.
+     */
+    public List<MeasureUnit> splitToSingleUnits() {
+        final ArrayList<SingleUnitImpl> singleUnits = getMayBeReferenceOfMeasureUnitImpl().getSingleUnits();
+        List<MeasureUnit> result = new ArrayList<>(singleUnits.size());
+        for (SingleUnitImpl singleUnit : singleUnits) {
+            result.add(singleUnit.build());
+        }
+
+        return result;
+    }
 
     /**
      * {@inheritDoc}
@@ -115,8 +591,8 @@
         if (!(rhs instanceof MeasureUnit)) {
             return false;
         }
-        MeasureUnit c = (MeasureUnit) rhs;
-        return type.equals(c.type) && subType.equals(c.subType);
+
+        return this.getIdentifier().equals(((MeasureUnit) rhs).getIdentifier());
     }
 
     /**
@@ -1546,6 +2022,39 @@
         return new MeasureUnitProxy(type, subType);
     }
 
+    /**
+     *
+     * @return this object as a SingleUnitImpl.
+     * @throws UnsupportedOperationException if this object could not be converted to a single unit.
+     */
+    private SingleUnitImpl getSingleUnitImpl() {
+        if (measureUnitImpl == null) {
+            return MeasureUnitImpl.forIdentifier(getIdentifier()).getSingleUnitImpl();
+        }
+
+        return measureUnitImpl.getSingleUnitImpl();
+    }
+
+    /**
+     *
+     * @return this object in a MeasureUnitImpl form.
+     */
+    private MeasureUnitImpl getCopyOfMeasureUnitImpl() {
+        return this.measureUnitImpl == null ?
+                MeasureUnitImpl.forIdentifier(getIdentifier()) :
+                this.measureUnitImpl.clone();
+    }
+
+    /**
+     *
+     * @return this object in a MeasureUnitImpl form.
+     */
+    private MeasureUnitImpl getMayBeReferenceOfMeasureUnitImpl(){
+        return this.measureUnitImpl == null ?
+                MeasureUnitImpl.forIdentifier(getIdentifier()) :
+                this.measureUnitImpl;
+    }
+
     static final class MeasureUnitProxy implements Externalizable {
         private static final long serialVersionUID = -3910681415330989598L;
 
@@ -1586,4 +2095,4 @@
             return MeasureUnit.internalGetInstance(type, subType);
         }
     }
-}
+}
\ No newline at end of file
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java
index 71f90cc..3b1c424 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java
@@ -8,33 +8,6 @@
  */
 package com.ibm.icu.dev.test.format;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.io.Serializable;
-import java.lang.reflect.Field;
-import java.text.FieldPosition;
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeMap;
-
-import org.junit.Assert;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
 import com.ibm.icu.dev.test.TestFmwk;
 import com.ibm.icu.dev.test.serializable.FormatHandler;
 import com.ibm.icu.dev.test.serializable.SerializableTestUtility;
@@ -45,13 +18,17 @@
 import com.ibm.icu.text.MeasureFormat.FormatWidth;
 import com.ibm.icu.text.NumberFormat;
 import com.ibm.icu.util.Currency;
-import com.ibm.icu.util.CurrencyAmount;
-import com.ibm.icu.util.Measure;
-import com.ibm.icu.util.MeasureUnit;
-import com.ibm.icu.util.NoUnit;
-import com.ibm.icu.util.TimeUnit;
-import com.ibm.icu.util.TimeUnitAmount;
-import com.ibm.icu.util.ULocale;
+import com.ibm.icu.util.*;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.*;
+import java.lang.reflect.Field;
+import java.text.FieldPosition;
+import java.text.ParseException;
+import java.util.*;
 
 /**
  * See https://sites.google.com/site/icusite/processes/release/tasks/standards?pli=1
@@ -3485,4 +3462,405 @@
         fmt = MeasureFormat.getInstance(ULocale.forLanguageTag("da"), FormatWidth.NUMERIC);
         Assert.assertEquals("2.03,877", fmt.formatMeasures(fhours, fminutes));
     }
+
+    @Test
+    public void TestIdentifiers() {
+        class TestCase {
+            final String id;
+            final String normalized;
+
+            TestCase(String id, String normalized) {
+                this.id = id;
+                this.normalized = normalized;
+            }
+        }
+
+        TestCase cases[] = {
+                // Correctly normalized identifiers should not change
+                new TestCase("square-meter-per-square-meter", "square-meter-per-square-meter"),
+                new TestCase("kilogram-meter-per-square-meter-square-second",
+                        "kilogram-meter-per-square-meter-square-second"),
+                new TestCase("square-mile-and-square-foot", "square-mile-and-square-foot"),
+                new TestCase("square-foot-and-square-mile", "square-foot-and-square-mile"),
+                new TestCase("per-cubic-centimeter", "per-cubic-centimeter"),
+                new TestCase("per-kilometer", "per-kilometer"),
+
+                // Normalization of power and per
+                new TestCase(
+                        "pow2-foot-and-pow2-mile", "square-foot-and-square-mile"),
+                new TestCase(
+                        "gram-square-gram-per-dekagram", "cubic-gram-per-dekagram"),
+                new TestCase(
+                        "kilogram-per-meter-per-second", "kilogram-per-meter-second"),
+
+                // TODO(ICU-20920): Add more test cases once the proper ranking is available.
+        };
+
+
+        for (TestCase testCase : cases) {
+            MeasureUnit unit = MeasureUnit.forIdentifier(testCase.id);
+
+            final String actual = unit.getIdentifier();
+            assertEquals(testCase.id, testCase.normalized, actual);
+        }
+
+        assertEquals("for empty identifiers, the MeasureUnit will be null",
+                null, MeasureUnit.forIdentifier(""));
+    }
+
+    @Test
+    public void TestInvalidIdentifiers() {
+        final String inputs[] = {
+                "kilo",
+                "kilokilo",
+                "onekilo",
+                "meterkilo",
+                "meter-kilo",
+                "k",
+                "meter-",
+                "meter+",
+                "-meter",
+                "+meter",
+                "-kilometer",
+                "+kilometer",
+                "-pow2-meter",
+                "+pow2-meter",
+                "p2-meter",
+                "p4-meter",
+                "+",
+                "-",
+                "-mile",
+                "-and-mile",
+                "-per-mile",
+                "one",
+                "one-one",
+                "one-per-mile",
+                "one-per-cubic-centimeter",
+                "square--per-meter",
+                "metersecond", // Must have compound part in between single units
+
+                // Negative powers not supported in mixed units yet. TODO(CLDR-13701).
+                "per-hour-and-hertz",
+                "hertz-and-per-hour",
+
+                // Compound units not supported in mixed units yet. TODO(CLDR-13700).
+                "kilonewton-meter-and-newton-meter",
+        };
+
+        for (String input : inputs) {
+            try {
+                MeasureUnit.forIdentifier(input);
+                Assert.fail("An IllegalArgumentException must be thrown");
+            } catch (IllegalArgumentException e) {
+                continue;
+            }
+        }
+    }
+
+    @Test
+    public void TestCompoundUnitOperations() {
+        MeasureUnit.forIdentifier("kilometer-per-second-joule");
+
+        MeasureUnit kilometer = MeasureUnit.KILOMETER;
+        MeasureUnit cubicMeter = MeasureUnit.CUBIC_METER;
+        MeasureUnit meter = kilometer.withSIPrefix(MeasureUnit.SIPrefix.ONE);
+        MeasureUnit centimeter1 = kilometer.withSIPrefix(MeasureUnit.SIPrefix.CENTI);
+        MeasureUnit centimeter2 = meter.withSIPrefix(MeasureUnit.SIPrefix.CENTI);
+        MeasureUnit cubicDecimeter = cubicMeter.withSIPrefix(MeasureUnit.SIPrefix.DECI);
+
+        verifySingleUnit(kilometer, MeasureUnit.SIPrefix.KILO, 1, "kilometer");
+        verifySingleUnit(meter, MeasureUnit.SIPrefix.ONE, 1, "meter");
+        verifySingleUnit(centimeter1, MeasureUnit.SIPrefix.CENTI, 1, "centimeter");
+        verifySingleUnit(centimeter2, MeasureUnit.SIPrefix.CENTI, 1, "centimeter");
+        verifySingleUnit(cubicDecimeter, MeasureUnit.SIPrefix.DECI, 3, "cubic-decimeter");
+
+        assertTrue("centimeter equality", centimeter1.equals( centimeter2));
+        assertTrue("kilometer inequality", !centimeter1.equals( kilometer));
+
+        MeasureUnit squareMeter = meter.withDimensionality(2);
+        MeasureUnit overCubicCentimeter = centimeter1.withDimensionality(-3);
+        MeasureUnit quarticKilometer = kilometer.withDimensionality(4);
+        MeasureUnit overQuarticKilometer1 = kilometer.withDimensionality(-4);
+
+        verifySingleUnit(squareMeter, MeasureUnit.SIPrefix.ONE, 2, "square-meter");
+        verifySingleUnit(overCubicCentimeter, MeasureUnit.SIPrefix.CENTI, -3, "per-cubic-centimeter");
+        verifySingleUnit(quarticKilometer, MeasureUnit.SIPrefix.KILO, 4, "pow4-kilometer");
+        verifySingleUnit(overQuarticKilometer1, MeasureUnit.SIPrefix.KILO, -4, "per-pow4-kilometer");
+
+        assertTrue("power inequality", quarticKilometer != overQuarticKilometer1);
+
+        MeasureUnit overQuarticKilometer2 = quarticKilometer.reciprocal();
+        MeasureUnit overQuarticKilometer3 = kilometer.product(kilometer)
+                .product(kilometer)
+                .product(kilometer)
+                .reciprocal();
+        MeasureUnit overQuarticKilometer4 = meter.withDimensionality(4)
+                .reciprocal()
+                .withSIPrefix(MeasureUnit.SIPrefix.KILO);
+
+        verifySingleUnit(overQuarticKilometer2, MeasureUnit.SIPrefix.KILO, -4, "per-pow4-kilometer");
+        verifySingleUnit(overQuarticKilometer3, MeasureUnit.SIPrefix.KILO, -4, "per-pow4-kilometer");
+        verifySingleUnit(overQuarticKilometer4, MeasureUnit.SIPrefix.KILO, -4, "per-pow4-kilometer");
+
+        assertTrue("reciprocal equality", overQuarticKilometer1.equals(overQuarticKilometer2));
+        assertTrue("reciprocal equality", overQuarticKilometer1.equals(overQuarticKilometer3));
+        assertTrue("reciprocal equality", overQuarticKilometer1.equals(overQuarticKilometer4));
+
+        MeasureUnit kiloSquareSecond = MeasureUnit.SECOND
+                .withDimensionality(2).withSIPrefix(MeasureUnit.SIPrefix.KILO);
+        MeasureUnit meterSecond = meter.product(kiloSquareSecond);
+        MeasureUnit cubicMeterSecond1 = meter.withDimensionality(3).product(kiloSquareSecond);
+        MeasureUnit centimeterSecond1 = meter.withSIPrefix(MeasureUnit.SIPrefix.CENTI).product(kiloSquareSecond);
+        MeasureUnit secondCubicMeter = kiloSquareSecond.product(meter.withDimensionality(3));
+        MeasureUnit secondCentimeter = kiloSquareSecond.product(meter.withSIPrefix(MeasureUnit.SIPrefix.CENTI));
+        MeasureUnit secondCentimeterPerKilometer = secondCentimeter.product(kilometer.reciprocal());
+
+        verifySingleUnit(kiloSquareSecond, MeasureUnit.SIPrefix.KILO, 2, "square-kilosecond");
+        String meterSecondSub[] = {
+                "meter", "square-kilosecond"
+        };
+        verifyCompoundUnit(meterSecond, "meter-square-kilosecond",
+                meterSecondSub, meterSecondSub.length);
+        String cubicMeterSecond1Sub[] = {
+                "cubic-meter", "square-kilosecond"
+        };
+        verifyCompoundUnit(cubicMeterSecond1, "cubic-meter-square-kilosecond",
+                cubicMeterSecond1Sub, cubicMeterSecond1Sub.length);
+        String centimeterSecond1Sub[] = {
+                "centimeter", "square-kilosecond"
+        };
+        verifyCompoundUnit(centimeterSecond1, "centimeter-square-kilosecond",
+                centimeterSecond1Sub, centimeterSecond1Sub.length);
+        String secondCubicMeterSub[] = {
+                "cubic-meter", "square-kilosecond"
+        };
+        verifyCompoundUnit(secondCubicMeter, "cubic-meter-square-kilosecond",
+                secondCubicMeterSub, secondCubicMeterSub.length);
+        String secondCentimeterSub[] = {
+                "centimeter", "square-kilosecond"
+        };
+        verifyCompoundUnit(secondCentimeter, "centimeter-square-kilosecond",
+                secondCentimeterSub, secondCentimeterSub.length);
+        String secondCentimeterPerKilometerSub[] = {
+                "centimeter", "square-kilosecond", "per-kilometer"
+        };
+        verifyCompoundUnit(secondCentimeterPerKilometer, "centimeter-square-kilosecond-per-kilometer",
+                secondCentimeterPerKilometerSub, secondCentimeterPerKilometerSub.length);
+
+        assertTrue("reordering equality", cubicMeterSecond1.equals(secondCubicMeter));
+        assertTrue("additional simple units inequality", !secondCubicMeter.equals(secondCentimeter));
+
+        // Don't allow get/set power or SI prefix on compound units
+        try {
+            meterSecond.getDimensionality();
+            fail("UnsupportedOperationException must be thrown");
+        } catch (UnsupportedOperationException e) {
+            // Expecting an exception to be thrown
+        }
+
+        try {
+            meterSecond.withDimensionality(3);
+            fail("UnsupportedOperationException must be thrown");
+        } catch (UnsupportedOperationException e) {
+            // Expecting an exception to be thrown
+        }
+
+        try {
+            meterSecond.getSIPrefix();
+            fail("UnsupportedOperationException must be thrown");
+        } catch (UnsupportedOperationException e) {
+            // Expecting an exception to be thrown
+        }
+
+        try {
+            meterSecond.withSIPrefix(MeasureUnit.SIPrefix.CENTI);
+            fail("UnsupportedOperationException must be thrown");
+        } catch (UnsupportedOperationException e) {
+            // Expecting an exception to be thrown
+        }
+
+        MeasureUnit footInch = MeasureUnit.forIdentifier("foot-and-inch");
+        MeasureUnit inchFoot = MeasureUnit.forIdentifier("inch-and-foot");
+
+        String footInchSub[] = {
+                "foot", "inch"
+        };
+        verifyMixedUnit(footInch, "foot-and-inch",
+                footInchSub, footInchSub.length);
+        String inchFootSub[] = {
+                "inch", "foot"
+        };
+        verifyMixedUnit(inchFoot, "inch-and-foot",
+                inchFootSub, inchFootSub.length);
+
+        assertTrue("order matters inequality", !footInch.equals(inchFoot));
+
+
+        MeasureUnit dimensionless  = NoUnit.BASE;
+        MeasureUnit dimensionless2 = MeasureUnit.forIdentifier("");
+        assertEquals("dimensionless equality", dimensionless, dimensionless2);
+
+        // We support starting from an "identity" MeasureUnit and then combining it
+        // with others via product:
+        MeasureUnit kilometer2 = kilometer.product(dimensionless);
+
+        verifySingleUnit(kilometer2, MeasureUnit.SIPrefix.KILO, 1, "kilometer");
+        assertTrue("kilometer equality", kilometer.equals(kilometer2));
+
+        // Test out-of-range powers
+        MeasureUnit power15 = MeasureUnit.forIdentifier("pow15-kilometer");
+        verifySingleUnit(power15, MeasureUnit.SIPrefix.KILO, 15, "pow15-kilometer");
+
+        try {
+            MeasureUnit.forIdentifier("pow16-kilometer");
+            fail("An IllegalArgumentException must be thrown");
+        } catch (IllegalArgumentException e) {
+            // Expecting an exception to be thrown
+        }
+
+        try {
+            power15.product(kilometer);
+            fail("An IllegalArgumentException must be thrown");
+        } catch (IllegalArgumentException e) {
+            // Expecting an exception to be thrown
+        }
+
+        MeasureUnit powerN15 = MeasureUnit.forIdentifier("per-pow15-kilometer");
+        verifySingleUnit(powerN15, MeasureUnit.SIPrefix.KILO, -15, "per-pow15-kilometer");
+
+        try {
+            MeasureUnit.forIdentifier("per-pow16-kilometer");
+            fail("An IllegalArgumentException must be thrown");
+        } catch (IllegalArgumentException e) {
+            // Expecting an exception to be thrown
+        }
+
+        try {
+            powerN15.product(overQuarticKilometer1);
+            fail("An IllegalArgumentException must be thrown");
+        } catch (IllegalArgumentException e) {
+            // Expecting an exception to be thrown
+        }
+    }
+
+    @Test
+    public void TestDimensionlessBehaviour() {
+        MeasureUnit dimensionless = MeasureUnit.forIdentifier("");
+        MeasureUnit dimensionless2 = NoUnit.BASE;
+        MeasureUnit dimensionless3 = null;
+        MeasureUnit dimensionless4 = MeasureUnit.forIdentifier(null);
+
+        assertEquals("dimensionless must be equals", dimensionless, dimensionless2);
+        assertEquals("dimensionless must be equals", dimensionless2, dimensionless3);
+        assertEquals("dimensionless must be equals", dimensionless3, dimensionless4);
+
+        // product(dimensionless)
+        MeasureUnit mile = MeasureUnit.MILE;
+        mile = mile.product(dimensionless);
+        verifySingleUnit(mile, MeasureUnit.SIPrefix.ONE, 1, "mile");
+    }
+
+    private void verifySingleUnit(MeasureUnit singleMeasureUnit, MeasureUnit.SIPrefix prefix, int power, String identifier) {
+        assertEquals(identifier + ": SI prefix", prefix, singleMeasureUnit.getSIPrefix());
+
+        assertEquals(identifier + ": Power", power, singleMeasureUnit.getDimensionality());
+
+        assertEquals(identifier + ": Identifier", identifier, singleMeasureUnit.getIdentifier());
+
+        assertTrue(identifier + ": Constructor", singleMeasureUnit.equals(MeasureUnit.forIdentifier(identifier)));
+
+        assertEquals(identifier + ": Complexity", MeasureUnit.Complexity.SINGLE, singleMeasureUnit.getComplexity());
+    }
+
+
+    // Kilogram is a "base unit", although it's also "gram" with a kilo- prefix.
+    // This tests that it is handled in the preferred manner.
+    @Test
+    public void TestKilogramIdentifier() {
+        // SI unit of mass
+        MeasureUnit kilogram = MeasureUnit.forIdentifier("kilogram");
+        // Metric mass unit
+        MeasureUnit gram = MeasureUnit.forIdentifier("gram");
+        // Microgram: still a built-in type
+        MeasureUnit microgram = MeasureUnit.forIdentifier("microgram");
+        // Nanogram: not a built-in type at this time
+        MeasureUnit nanogram = MeasureUnit.forIdentifier("nanogram");
+
+        assertEquals("parsed kilogram equals built-in kilogram", MeasureUnit.KILOGRAM.getType(),
+                kilogram.getType());
+        assertEquals("parsed kilogram equals built-in kilogram", MeasureUnit.KILOGRAM.getSubtype(),
+                kilogram.getSubtype());
+        assertEquals("parsed gram equals built-in gram", MeasureUnit.GRAM.getType(), gram.getType());
+        assertEquals("parsed gram equals built-in gram", MeasureUnit.GRAM.getSubtype(),
+                gram.getSubtype());
+        assertEquals("parsed microgram equals built-in microgram", MeasureUnit.MICROGRAM.getType(),
+                microgram.getType());
+        assertEquals("parsed microgram equals built-in microgram", MeasureUnit.MICROGRAM.getSubtype(),
+                microgram.getSubtype());
+        assertEquals("nanogram", null, nanogram.getType());
+        assertEquals("nanogram", "nanogram", nanogram.getIdentifier());
+
+        assertEquals("prefix of kilogram", MeasureUnit.SIPrefix.KILO, kilogram.getSIPrefix());
+        assertEquals("prefix of gram", MeasureUnit.SIPrefix.ONE, gram.getSIPrefix());
+        assertEquals("prefix of microgram", MeasureUnit.SIPrefix.MICRO, microgram.getSIPrefix());
+        assertEquals("prefix of nanogram", MeasureUnit.SIPrefix.NANO, nanogram.getSIPrefix());
+
+        MeasureUnit tmp = kilogram.withSIPrefix(MeasureUnit.SIPrefix.MILLI);
+        assertEquals("Kilogram + milli should be milligram, got: " + tmp.getIdentifier(),
+                MeasureUnit.MILLIGRAM.getIdentifier(), tmp.getIdentifier());
+    }
+
+    private void verifyCompoundUnit(
+            MeasureUnit unit,
+            String identifier,
+            String subIdentifiers[],
+            int subIdentifierCount) {
+        assertEquals(identifier + ": Identifier",
+                identifier,
+                unit.getIdentifier());
+
+        assertTrue(identifier + ": Constructor",
+                unit.equals(MeasureUnit.forIdentifier(identifier)));
+
+        assertEquals(identifier + ": Complexity",
+                MeasureUnit.Complexity.COMPOUND,
+                unit.getComplexity());
+
+        List<MeasureUnit> subUnits = unit.splitToSingleUnits();
+        assertEquals(identifier + ": Length", subIdentifierCount, subUnits.size());
+        for (int i = 0; ; i++) {
+            if (i >= subIdentifierCount || i >= subUnits.size()) break;
+            assertEquals(identifier + ": Sub-unit #" + i,
+                    subIdentifiers[i],
+                    subUnits.get(i).getIdentifier());
+            assertEquals(identifier + ": Sub-unit Complexity",
+                    MeasureUnit.Complexity.SINGLE,
+                    subUnits.get(i).getComplexity());
+        }
+    }
+
+    private void verifyMixedUnit(
+            MeasureUnit unit,
+            String identifier,
+            String subIdentifiers[],
+            int subIdentifierCount) {
+        assertEquals(identifier + ": Identifier",
+                identifier,
+                unit.getIdentifier());
+        assertTrue(identifier + ": Constructor",
+                unit.equals(MeasureUnit.forIdentifier(identifier)));
+
+        assertEquals(identifier + ": Complexity",
+                MeasureUnit.Complexity.MIXED,
+                unit.getComplexity());
+
+        List<MeasureUnit> subUnits = unit.splitToSingleUnits();
+        assertEquals(identifier + ": Length", subIdentifierCount, subUnits.size());
+        for (int i = 0; ; i++) {
+            if (i >= subIdentifierCount || i >= subUnits.size()) break;
+            assertEquals(identifier + ": Sub-unit #" + i,
+                    subIdentifiers[i],
+                    subUnits.get(i).getIdentifier());
+        }
+    }
 }