/
githubmirror
/
riscv-port
Обзор
Документация
Войти
/
githubmirror
/
riscv-port
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/java.base/share/classes/java/text/AttributedString.java
1 036 строк
39 KB
Daniel Gredler
8380794: AttributedString performance
12 май 2026, 02:24
12 май 2026, 02:24
975e209
Код
Авторство
О чём код?
/* * Copyright (c) 1997, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package java.text; import java.util.*; import java.text.AttributedCharacterIterator.Attribute; /** * An AttributedString holds text and related attribute information. It * may be used as the actual data storage in some cases where a text * reader wants to access attributed text through the AttributedCharacterIterator * interface. * * <p> * An attribute is a key/value pair, identified by the key. No two * attributes on a given character can have the same key. * * <p>The values for an attribute are immutable, or must not be mutated * by clients or storage. They are always passed by reference, and not * cloned. * * @see AttributedCharacterIterator * @see Annotation * @since 1.2 */ public class AttributedString { // field holding the text String text; // Fields holding run attribute information. // Run attributes are organized by run. // Arrays are always of equal lengths (the current capacity). // Since there are not yet Lists of unboxed int, we use arrays. private static final int INITIAL_CAPACITY = 10; int runCount; // actual number of runs, <= current capacity int[] runStarts; // start index for each run Map<Attribute,Object>[] runAttributes; // attributes for each run /** * Constructs an AttributedString instance with the given * AttributedCharacterIterators. * * @param iterators AttributedCharacterIterators to construct * AttributedString from. * @throws NullPointerException if iterators is null */ AttributedString(AttributedCharacterIterator[] iterators) { if (iterators == null) { throw new NullPointerException("Iterators must not be null"); } if (iterators.length == 0) { text = ""; } else { // Build the String contents StringBuilder buffer = new StringBuilder(); for (int counter = 0; counter < iterators.length; counter++) { appendContents(buffer, iterators[counter]); } text = buffer.toString(); if (!text.isEmpty()) { // Determine the runs, creating a new run when the attributes // differ. int offset = 0; Map<Attribute,Object> last = null; for (int counter = 0; counter < iterators.length; counter++) { AttributedCharacterIterator iterator = iterators[counter]; int start = iterator.getBeginIndex(); int end = iterator.getEndIndex(); int index = start; while (index < end) { iterator.setIndex(index); Map<Attribute,Object> attrs = iterator.getAttributes(); if (mapsDiffer(last, attrs)) { setAttributes(attrs, index - start + offset); } last = attrs; index = iterator.getRunLimit(); } offset += (end - start); } } } } /** * Constructs an AttributedString instance with the given text. * @param text The text for this attributed string. * @throws NullPointerException if {@code text} is null. */ public AttributedString(String text) { if (text == null) { throw new NullPointerException(); } this.text = text; } /** * Constructs an AttributedString instance with the given text and attributes. * @param text The text for this attributed string. * @param attributes The attributes that apply to the entire string. * @throws NullPointerException if {@code text} or * {@code attributes} is null. * @throws IllegalArgumentException if the text has length 0 * and the attributes parameter is not an empty Map (attributes * cannot be applied to a 0-length range). */ public AttributedString(String text, Map<? extends Attribute, ?> attributes) { if (text == null || attributes == null) { throw new NullPointerException(); } this.text = text; if (text.isEmpty()) { if (attributes.isEmpty()) return; throw new IllegalArgumentException("Can't add attribute to 0-length text"); } int attributeCount = attributes.size(); if (attributeCount > 0) { createRunAttributeDataArrays(); runAttributes[0] = new HashMap<>(attributes); } } /** * Constructs an AttributedString instance with the given attributed * text represented by AttributedCharacterIterator. * @param text The text for this attributed string. * @throws NullPointerException if {@code text} is null. */ public AttributedString(AttributedCharacterIterator text) { // If performance is critical, this constructor should be // implemented here rather than invoking the constructor for a // subrange. We can avoid some range checking in the loops. this(text, text.getBeginIndex(), text.getEndIndex(), null); } /** * Constructs an AttributedString instance with the subrange of * the given attributed text represented by * AttributedCharacterIterator. If the given range produces an * empty text, all attributes will be discarded. Note that any * attributes wrapped by an Annotation object are discarded for a * subrange of the original attribute range. * * @param text The text for this attributed string. * @param beginIndex Index of the first character of the range. * @param endIndex Index of the character following the last character * of the range. * @throws NullPointerException if {@code text} is null. * @throws IllegalArgumentException if the subrange given by * beginIndex and endIndex is out of the text range. * @see java.text.Annotation */ public AttributedString(AttributedCharacterIterator text, int beginIndex, int endIndex) { this(text, beginIndex, endIndex, null); } /** * Constructs an AttributedString instance with the subrange of * the given attributed text represented by * AttributedCharacterIterator. Only attributes that match the * given attributes will be incorporated into the instance. If the * given range produces an empty text, all attributes will be * discarded. Note that any attributes wrapped by an Annotation * object are discarded for a subrange of the original attribute * range. * * @param text The text for this attributed string. * @param beginIndex Index of the first character of the range. * @param endIndex Index of the character following the last character * of the range. * @param attributes Specifies attributes to be extracted * from the text. If null is specified, all available attributes will * be used. * @throws NullPointerException if {@code text} is null. * @throws IllegalArgumentException if the subrange given by * beginIndex and endIndex is out of the text range. * @see java.text.Annotation */ @SuppressWarnings("this-escape") public AttributedString(AttributedCharacterIterator text, int beginIndex, int endIndex, Attribute[] attributes) { if (text == null) { throw new NullPointerException(); } // Validate the given subrange int textBeginIndex = text.getBeginIndex(); int textEndIndex = text.getEndIndex(); if (beginIndex < textBeginIndex || endIndex > textEndIndex || beginIndex > endIndex) throw new IllegalArgumentException("Invalid substring range"); // Copy the given string StringBuilder textBuilder = new StringBuilder(); text.setIndex(beginIndex); for (char c = text.current(); text.getIndex() < endIndex; c = text.next()) textBuilder.append(c); this.text = textBuilder.toString(); if (beginIndex == endIndex) return; // Select attribute keys to be taken care of HashSet<Attribute> keys = new HashSet<>(); Set<Attribute> textKeys = text.getAllAttributeKeys(); if (attributes == null) { keys.addAll(textKeys); } else { for (int i = 0; i < attributes.length; i++) keys.add(attributes[i]); keys.retainAll(textKeys); } if (keys.isEmpty()) return; // Get and set attribute runs for each attribute name. Need to // scan from the top of the text so that we can discard any // Annotation that is no longer applied to a subset text segment. for (Attribute attributeKey : keys) { text.setIndex(textBeginIndex); while (text.getIndex() < endIndex) { int start = text.getRunStart(attributeKey); int limit = text.getRunLimit(attributeKey); Object value = text.getAttribute(attributeKey); if (value != null) { if (value instanceof Annotation) { if (start >= beginIndex && limit <= endIndex) { addAttribute(attributeKey, value, start - beginIndex, limit - beginIndex); } else { if (limit > endIndex) break; } } else { // if the run is beyond the given (subset) range, we // don't need to process further. if (start >= endIndex) break; if (limit > beginIndex) { // attribute is applied to any subrange if (start < beginIndex) start = beginIndex; if (limit > endIndex) limit = endIndex; if (start != limit) { addAttribute(attributeKey, value, start - beginIndex, limit - beginIndex); } } } } text.setIndex(limit); } } } /** * Adds an attribute to the entire string. * @param attribute the attribute key * @param value the value of the attribute; may be null * @throws NullPointerException if {@code attribute} is null. * @throws IllegalArgumentException if the AttributedString has length 0 * (attributes cannot be applied to a 0-length range). */ public void addAttribute(Attribute attribute, Object value) { if (attribute == null) { throw new NullPointerException(); } int len = length(); if (len == 0) { throw new IllegalArgumentException("Can't add attribute to 0-length text"); } addAttributeImpl(attribute, value, 0, len); } /** * Adds an attribute to a subrange of the string. * @param attribute the attribute key * @param value The value of the attribute. May be null. * @param beginIndex Index of the first character of the range. * @param endIndex Index of the character following the last character of the range. * @throws NullPointerException if {@code attribute} is null. * @throws IllegalArgumentException if beginIndex is less than 0, endIndex is * greater than the length of the string, or beginIndex and endIndex together don't * define a non-empty subrange of the string. */ public void addAttribute(Attribute attribute, Object value, int beginIndex, int endIndex) { if (attribute == null) { throw new NullPointerException(); } if (beginIndex < 0 || endIndex > length() || beginIndex >= endIndex) { throw new IllegalArgumentException("Invalid substring range"); } addAttributeImpl(attribute, value, beginIndex, endIndex); } /** * Adds a set of attributes to a subrange of the string. * @param attributes The attributes to be added to the string. * @param beginIndex Index of the first character of the range. * @param endIndex Index of the character following the last * character of the range. * @throws NullPointerException if {@code attributes} is null. * @throws IllegalArgumentException if beginIndex is less than * 0, endIndex is greater than the length of the string, or * beginIndex and endIndex together don't define a non-empty * subrange of the string and the attributes parameter is not an * empty Map. */ public void addAttributes(Map<? extends Attribute, ?> attributes, int beginIndex, int endIndex) { if (attributes == null) { throw new NullPointerException(); } if (beginIndex < 0 || endIndex > length() || beginIndex > endIndex) { throw new IllegalArgumentException("Invalid substring range"); } if (beginIndex == endIndex) { if (attributes.isEmpty()) return; throw new IllegalArgumentException("Can't add attribute to 0-length text"); } // make sure we have run attribute data arrays if (runCount == 0) { createRunAttributeDataArrays(); } // break up runs if necessary int beginRunIndex = ensureRunBreak(beginIndex); int endRunIndex = ensureRunBreak(endIndex); for (Map.Entry<? extends Attribute, ?> entry : attributes.entrySet()) { addAttributeRunData(entry.getKey(), entry.getValue(), beginRunIndex, endRunIndex); } } private synchronized void addAttributeImpl(Attribute attribute, Object value, int beginIndex, int endIndex) { // make sure we have run attribute data arrays if (runCount == 0) { createRunAttributeDataArrays(); } // break up runs if necessary int beginRunIndex = ensureRunBreak(beginIndex); int endRunIndex = ensureRunBreak(endIndex); addAttributeRunData(attribute, value, beginRunIndex, endRunIndex); } private final void createRunAttributeDataArrays() { // use temporary variables so things remain consistent in case of an exception int[] newRunStarts = new int[INITIAL_CAPACITY]; @SuppressWarnings("unchecked") Map<Attribute, Object>[] newRunAttributes = (Map<Attribute, Object>[]) new Map<?, ?>[INITIAL_CAPACITY]; runStarts = newRunStarts; runAttributes = newRunAttributes; runCount = 1; // assume initial run starting at index 0 } // ensure there's a run break at offset, return the index of the run private final int ensureRunBreak(int offset) { return ensureRunBreak(offset, true); } /** * Ensures there is a run break at offset, returning the index of * the run. If this results in splitting a run, two things can happen: * <ul> * <li>If copyAttrs is true, the attributes from the existing run * will be placed in both of the newly created runs. * <li>If copyAttrs is false, the attributes from the existing run * will NOT be copied to the run to the right (>= offset) of the break, * but will exist on the run to the left (< offset). * </ul> */ private final int ensureRunBreak(int offset, boolean copyAttrs) { if (offset == length()) { return runCount; } // search for the run index where this offset should be int runIndex = 0; while (runIndex < runCount && runStarts[runIndex] < offset) { runIndex++; } // if the offset is at a run start already, we're done if (runIndex < runCount && runStarts[runIndex] == offset) { return runIndex; } // we'll have to break up a run // first, make sure we have enough space in our arrays int currentCapacity = runStarts.length; if (runCount == currentCapacity) { // We need to resize - we grow capacity by 25%. int newCapacity = currentCapacity + (currentCapacity >> 2); // use temporary variables so things remain consistent in case of an exception int[] newRunStarts = Arrays.copyOf(runStarts, newCapacity); Map<Attribute, Object>[] newRunAttributes = Arrays.copyOf(runAttributes, newCapacity); runStarts = newRunStarts; runAttributes = newRunAttributes; } // make copies of the attribute information of the old run that the new one used to be part of // use temporary variables so things remain consistent in case of an exception Map<Attribute, Object> newRunAttributes = null; if (copyAttrs) { Map<Attribute, Object> oldRunAttributes = runAttributes[runIndex - 1]; if (oldRunAttributes != null) { newRunAttributes = new HashMap<>(oldRunAttributes); } } // now actually break up the run runCount++; for (int i = runCount - 1; i > runIndex; i--) { runStarts[i] = runStarts[i - 1]; runAttributes[i] = runAttributes[i - 1]; } runStarts[runIndex] = offset; runAttributes[runIndex] = newRunAttributes; return runIndex; } // add the attribute attribute/value to all runs where beginRunIndex <= runIndex < endRunIndex private void addAttributeRunData(Attribute attribute, Object value, int beginRunIndex, int endRunIndex) { for (int i = beginRunIndex; i < endRunIndex; i++) { Map<Attribute, Object> attributes = runAttributes[i]; if (attributes == null) { Map<Attribute, Object> newRunAttributes = new HashMap<>(); runAttributes[i] = newRunAttributes; attributes = newRunAttributes; } attributes.put(attribute, value); } } /** * Creates an AttributedCharacterIterator instance that provides access to the entire contents of * this string. * * @return An iterator providing access to the text and its attributes. */ public AttributedCharacterIterator getIterator() { return getIterator(null, 0, length()); } /** * Creates an AttributedCharacterIterator instance that provides access to * selected contents of this string. * Information about attributes not listed in attributes that the * implementor may have need not be made accessible through the iterator. * If the list is null, all available attribute information should be made * accessible. * * @param attributes a list of attributes that the client is interested in * @return an iterator providing access to the entire text and its selected attributes */ public AttributedCharacterIterator getIterator(Attribute[] attributes) { return getIterator(attributes, 0, length()); } /** * Creates an AttributedCharacterIterator instance that provides access to * selected contents of this string. * Information about attributes not listed in attributes that the * implementor may have need not be made accessible through the iterator. * If the list is null, all available attribute information should be made * accessible. * * @param attributes a list of attributes that the client is interested in * @param beginIndex the index of the first character * @param endIndex the index of the character following the last character * @return an iterator providing access to the text and its attributes * @throws IllegalArgumentException if beginIndex is less than 0, * endIndex is greater than the length of the string, or beginIndex is * greater than endIndex. */ public AttributedCharacterIterator getIterator(Attribute[] attributes, int beginIndex, int endIndex) { return new AttributedStringIterator(attributes, beginIndex, endIndex); } // all (with the exception of length) reading operations are private, // since AttributedString instances are accessed through iterators. // length is package private so that CharacterIteratorFieldDelegate can // access it without creating an AttributedCharacterIterator. int length() { return text.length(); } private char charAt(int index) { return text.charAt(index); } private synchronized Object getAttribute(Attribute attribute, int runIndex) { Map<Attribute, Object> currentRunAttributes = runAttributes[runIndex]; if (currentRunAttributes == null) { return null; } return currentRunAttributes.get(attribute); } // gets an attribute value, but returns an annotation only if it's range does not extend outside the range beginIndex..endIndex private Object getAttributeCheckRange(Attribute attribute, int runIndex, int beginIndex, int endIndex) { Object value = getAttribute(attribute, runIndex); if (value instanceof Annotation) { // need to check whether the annotation's range extends outside the iterator's range if (beginIndex > 0) { int currIndex = runIndex; int runStart = runStarts[currIndex]; while (runStart >= beginIndex && Objects.equals(value, getAttribute(attribute, currIndex - 1))) { currIndex--; runStart = runStarts[currIndex]; } if (runStart < beginIndex) { // annotation's range starts before iterator's range return null; } } int textLength = length(); if (endIndex < textLength) { int currIndex = runIndex; int runLimit = (currIndex < runCount - 1) ? runStarts[currIndex + 1] : textLength; while (runLimit <= endIndex && Objects.equals(value, getAttribute(attribute, currIndex + 1))) { currIndex++; runLimit = (currIndex < runCount - 1) ? runStarts[currIndex + 1] : textLength; } if (runLimit > endIndex) { // annotation's range ends after iterator's range return null; } } // annotation's range is subrange of iterator's range, // so we can return the value } return value; } // returns whether all specified attributes have equal values in the runs with the given indices private boolean attributeValuesMatch(Set<? extends Attribute> attributes, int runIndex1, int runIndex2) { for (Attribute key : attributes) { if (!Objects.equals(getAttribute(key, runIndex1), getAttribute(key, runIndex2))) { return false; } } return true; } /** * Appends the contents of the CharacterIterator iterator into the * StringBuilder buf. */ private final void appendContents(StringBuilder buf, CharacterIterator iterator) { int index = iterator.getBeginIndex(); int end = iterator.getEndIndex(); while (index < end) { iterator.setIndex(index++); buf.append(iterator.current()); } } /** * Sets the attributes for the range from offset to the next run break * (typically the end of the text) to the ones specified in attrs. * This is only meant to be called from the constructor! */ private void setAttributes(Map<Attribute, Object> attrs, int offset) { if (runCount == 0) { createRunAttributeDataArrays(); } int index = ensureRunBreak(offset, false); if (attrs != null && !attrs.isEmpty()) { runAttributes[index] = new HashMap<>(attrs); } } /** * Returns true if the attributes specified in last and attrs differ. */ private static <K,V> boolean mapsDiffer(Map<K, V> last, Map<K, V> attrs) { if (last == null) { return (attrs != null && attrs.size() > 0); } return (!last.equals(attrs)); } // the iterator class associated with this string class private final class AttributedStringIterator implements AttributedCharacterIterator { // note on synchronization: // we don't synchronize on the iterator, assuming that an iterator is only used in one thread. // we do synchronize access to the AttributedString however, since it's more likely to be shared between threads. // start and end index for our iteration private int beginIndex; private int endIndex; // attributes that our client is interested in private Attribute[] relevantAttributes; // the current index for our iteration // invariant: beginIndex <= currentIndex <= endIndex private int currentIndex; // information about the run that includes currentIndex private int currentRunIndex; private int currentRunStart; private int currentRunLimit; // constructor AttributedStringIterator(Attribute[] attributes, int beginIndex, int endIndex) { if (beginIndex < 0 || beginIndex > endIndex || endIndex > length()) { throw new IllegalArgumentException("Invalid substring range"); } this.beginIndex = beginIndex; this.endIndex = endIndex; this.currentIndex = beginIndex; updateRunInfo(); if (attributes != null) { relevantAttributes = attributes.clone(); } } // Object methods. See documentation in that class. @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof AttributedStringIterator that)) { return false; } if (AttributedString.this != that.getString()) return false; if (currentIndex != that.currentIndex || beginIndex != that.beginIndex || endIndex != that.endIndex) return false; return true; } @Override public int hashCode() { return text.hashCode() ^ currentIndex ^ beginIndex ^ endIndex; } public Object clone() { try { AttributedStringIterator other = (AttributedStringIterator) super.clone(); return other; } catch (CloneNotSupportedException e) { throw new InternalError(e); } } // CharacterIterator methods. See documentation in that interface. public char first() { return internalSetIndex(beginIndex); } public char last() { if (endIndex == beginIndex) { return internalSetIndex(endIndex); } else { return internalSetIndex(endIndex - 1); } } public char current() { if (currentIndex == endIndex) { return DONE; } else { return charAt(currentIndex); } } public char next() { if (currentIndex < endIndex) { return internalSetIndex(currentIndex + 1); } else { return DONE; } } public char previous() { if (currentIndex > beginIndex) { return internalSetIndex(currentIndex - 1); } else { return DONE; } } public char setIndex(int position) { if (position < beginIndex || position > endIndex) throw new IllegalArgumentException("Invalid index"); return internalSetIndex(position); } public int getBeginIndex() { return beginIndex; } public int getEndIndex() { return endIndex; } public int getIndex() { return currentIndex; } // AttributedCharacterIterator methods. See documentation in that interface. public int getRunStart() { return currentRunStart; } public int getRunStart(Attribute attribute) { if (currentRunStart == beginIndex || currentRunIndex == -1) { return currentRunStart; } else { Object value = getAttribute(attribute); int runStart = currentRunStart; int runIndex = currentRunIndex; while (runStart > beginIndex && Objects.equals(value, AttributedString.this.getAttribute(attribute, runIndex - 1))) { runIndex--; runStart = runStarts[runIndex]; } if (runStart < beginIndex) { runStart = beginIndex; } return runStart; } } public int getRunStart(Set<? extends Attribute> attributes) { if (currentRunStart == beginIndex || currentRunIndex == -1) { return currentRunStart; } else { int runStart = currentRunStart; int runIndex = currentRunIndex; while (runStart > beginIndex && AttributedString.this.attributeValuesMatch(attributes, currentRunIndex, runIndex - 1)) { runIndex--; runStart = runStarts[runIndex]; } if (runStart < beginIndex) { runStart = beginIndex; } return runStart; } } public int getRunLimit() { return currentRunLimit; } public int getRunLimit(Attribute attribute) { if (currentRunLimit == endIndex || currentRunIndex == -1) { return currentRunLimit; } else { Object value = getAttribute(attribute); int runLimit = currentRunLimit; int runIndex = currentRunIndex; while (runLimit < endIndex && Objects.equals(value, AttributedString.this.getAttribute(attribute, runIndex + 1))) { runIndex++; runLimit = runIndex < runCount - 1 ? runStarts[runIndex + 1] : endIndex; } if (runLimit > endIndex) { runLimit = endIndex; } return runLimit; } } public int getRunLimit(Set<? extends Attribute> attributes) { if (currentRunLimit == endIndex || currentRunIndex == -1) { return currentRunLimit; } else { int runLimit = currentRunLimit; int runIndex = currentRunIndex; while (runLimit < endIndex && AttributedString.this.attributeValuesMatch(attributes, currentRunIndex, runIndex + 1)) { runIndex++; runLimit = runIndex < runCount - 1 ? runStarts[runIndex + 1] : endIndex; } if (runLimit > endIndex) { runLimit = endIndex; } return runLimit; } } public Map<Attribute,Object> getAttributes() { if (runAttributes == null || currentRunIndex == -1 || runAttributes[currentRunIndex] == null) { // ??? would be nice to return null, but current spec doesn't allow it // returning Hashtable saves AttributeMap from dealing with emptiness return new Hashtable<>(); } return new AttributeMap(currentRunIndex, beginIndex, endIndex); } public Set<Attribute> getAllAttributeKeys() { // ??? This should screen out attribute keys that aren't relevant to the client if (runAttributes == null) { // ??? would be nice to return null, but current spec doesn't allow it // returning HashSet saves us from dealing with emptiness return new HashSet<>(); } synchronized (AttributedString.this) { // ??? should try to create this only once, then update if necessary, // and give callers read-only view Set<Attribute> keys = new HashSet<>(); for (int i = 0; i < runCount; i++) { if (runStarts[i] < endIndex && (i == runCount - 1 || runStarts[i + 1] > beginIndex)) { Map<Attribute, Object> currentRunAttributes = runAttributes[i]; if (currentRunAttributes != null) { keys.addAll(currentRunAttributes.keySet()); } } } return keys; } } public Object getAttribute(Attribute attribute) { int runIndex = currentRunIndex; if (runIndex < 0) { return null; } return AttributedString.this.getAttributeCheckRange(attribute, runIndex, beginIndex, endIndex); } // internally used methods private AttributedString getString() { return AttributedString.this; } // set the current index, update information about the current run if necessary, // return the character at the current index private char internalSetIndex(int position) { currentIndex = position; if (position < currentRunStart || position >= currentRunLimit) { updateRunInfo(); } if (currentIndex == endIndex) { return DONE; } else { return charAt(position); } } // update the information about the current run private void updateRunInfo() { if (currentIndex == endIndex) { currentRunStart = currentRunLimit = endIndex; currentRunIndex = -1; } else { synchronized (AttributedString.this) { int runIndex = -1; while (runIndex < runCount - 1 && runStarts[runIndex + 1] <= currentIndex) runIndex++; currentRunIndex = runIndex; if (runIndex >= 0) { currentRunStart = runStarts[runIndex]; if (currentRunStart < beginIndex) currentRunStart = beginIndex; } else { currentRunStart = beginIndex; } if (runIndex < runCount - 1) { currentRunLimit = runStarts[runIndex + 1]; if (currentRunLimit > endIndex) currentRunLimit = endIndex; } else { currentRunLimit = endIndex; } } } } } // the map class associated with this string class, giving access to the attributes of one run private final class AttributeMap extends AbstractMap<Attribute,Object> { int runIndex; int beginIndex; int endIndex; AttributeMap(int runIndex, int beginIndex, int endIndex) { this.runIndex = runIndex; this.beginIndex = beginIndex; this.endIndex = endIndex; } public Set<Map.Entry<Attribute, Object>> entrySet() { HashSet<Map.Entry<Attribute, Object>> set = new HashSet<>(); synchronized (AttributedString.this) { Map<Attribute, Object> attributes = runAttributes[runIndex]; for (Map.Entry<Attribute, Object> entry : attributes.entrySet()) { Attribute key = entry.getKey(); Object value = entry.getValue(); if (value instanceof Annotation) { value = AttributedString.this.getAttributeCheckRange(key, runIndex, beginIndex, endIndex); if (value == null) { continue; } } set.add(new AttributeEntry(key, value)); } } return set; } public Object get(Object key) { return AttributedString.this.getAttributeCheckRange((Attribute) key, runIndex, beginIndex, endIndex); } } } class AttributeEntry implements Map.Entry<Attribute,Object> { private Attribute key; private Object value; AttributeEntry(Attribute key, Object value) { this.key = key; this.value = value; } @Override public boolean equals(Object o) { return o instanceof AttributeEntry other && other.key.equals(key) && Objects.equals(other.value, value); } public Attribute getKey() { return key; } public Object getValue() { return value; } public Object setValue(Object newValue) { throw new UnsupportedOperationException(); } @Override public int hashCode() { return key.hashCode() ^ Objects.hashCode(value); } public String toString() { return key.toString()+"="+value.toString(); } }