A JDK String.repeat megvalósítása Java 11-ben

A Java 11 óta. Van egy új metódus a String osztályban, az úgynevezett repeat. Igen, gyorsan ismétli a húrt. Most tippelj. Hány megvalósítási sor van benne? Egysoros? Stream-divatos egysoros? 5-7 sor? 9? 12? Több. Sokkal több. Ebben a cikkben azt szeretném megvizsgálni, hogy miért. Kész vagy?

Kezdje profiként

Az első sorokat a bejövő paraméterek ellenőrzésére szánjuk. A civilizálatlan barbárok negatív számot adhatnak. Meg kell védenünk a királyságot, és riasztanunk kell. A nulla hosszúságú bemeneti karakterlánc vagy a no-repeat azt jelenti, hogy nincs munka, ezért pihenjen, és adjon vissza egy üres karakterláncot. Ha az ismétlődések száma eggyel egyenlő, akkor az énre való hivatkozást adjuk vissza. Nem sértjük meg a kódot, mivel a karakterlánc megváltoztathatatlan.

if (count < 0) {
    throw new IllegalArgumentException("count is negative: "+count);
}
if (count == 1) {
    return this;
}
final int len = value.length;
if (len == 0 || count == 0) {
    return "";
}

Teljesítményteljesítmény első számú, egybetűs optimalizálás

Most hozzuk létre a feliratgenerátort. Képzelj el egy jelenetet, ahol emberek zuhannak a lyukba. A furat mélységével arányos hosszúságú „aaaaaaaaaaa”-t kell generálnunk. A terület szakértői azt javasolnák, hogy sokszor fűzzék hozzá ugyanazt a betűt a Stringhez, igaz? Ez helytelen. Ez a tökéletes eset a Array osztályhoz!

if (len == 1) {
    final byte[] single = new byte[count];
    Arrays.fill(single, value[0]);
    return new String(single, coder);
}

Lehet, hogy nem hiszi el, de az Arrays.fill kitölti a tömböt. Micsoda csavar, igaz? Lehet, hogy a belső módszer is bonyolult, de nem az. Valójában ez egy egyszerű hurok. Az előny az, hogy a memória már le van foglalva. Nem kell megrángatnunk a memóriát sok blokk összefűzésével.

for (int i = 0, len = a.length; i < len; i++)
    a[i] = val;

Kár. Nem tudunk mutatót visszaadni erre a tömbre, ezért vissza kell konvertálnunk a String-re – a C++-t nézem. Hé, de miért kell itt egy második érv, a kódoló?

new String(single, coder)

A vonós osztály testőrző memória

A memória olcsó, olcsó és gyors. Hacsak nem drága, mert nagy szükségünk van rá. 500 dollárt költeni jobb, mint 1000 dollárt, igaz? Ez az oka a kódolónak. Ha lehetséges, LATIN1 kódolást használunk, így a karakterek csak 8 bájtot fogyasztanak. Ha néhány karakter kívül esik a hatókörön, akkor az UTF16-ra térünk vissza. Megfigyelhetjük egy ismeretlen karakter számítását a String.valueOf metódusban:

public static String valueOf(char c) {
    if (COMPACT_STRINGS && StringLatin1.canEncode(c)) {
        return new String(StringLatin1.toBytes(c), LATIN1);
    }
    return new String(StringUTF16.toBytes(c), UTF16);
}

Az ismétlésmódszerben az újraszámítás elkerülése érdekében az eredeti karakterláncból adjuk át az értéket. Ha ismerjük a karaktereket, nem kell ellenőrizni. Nem fogunk új karakterkódokat hozzáadni azok ismétlésével. Nem kell dolgozni!

Nem jutsz át

Ismételten a memória gondozása prioritást élvez az olyan kritikus osztályok esetében, mint a String. A folyamat ismétlése bizonyos esetekben memóriahibákat fog kiadni, függetlenül attól, hogy mi történik. Megjósolhatjuk ezt? Elkerülhetjük-e az időveszteséget az amúgy is sikertelen műveletre? A mágikus labda vagy a tarot előrejelző rendszer működhet itt, de nem pontosak. Ehelyett számok alapján kiszámolhatjuk a várható hosszt.

if (Integer.MAX_VALUE / count < len) {
        throw new OutOfMemoryError("Repeating " + len + " bytes String " + count + " times will produce a String exceeding maximum size.");

Az Integer.MAX_VALUE 0x7fffffff, ami ²³¹-1. Ha az előállított String nagyobb lesz, mint ~2 GB, akkor azonnal összeomolhatunk.

Hölgyeim és uraim, várva várt sztárunk

Ismételten a memória gondozása prioritást élvez az olyan kritikus osztályok esetében, mint a String. Egyes esetekben az ismétlési folyamat memóriahibákat produkál, bármitől függetlenül. Megjósolhatjuk ezt? Tehát nem pazaroljuk az időt a műveletre, amely úgyis meghiúsul? A mágikus labda vagy a tarot előrejelző rendszer néha működhet, de nem minden esetben pontos. Ehelyett számok alapján kiszámolhatjuk a várható hosszt.

    final int limit = len * count;
    final byte[] multiple = new byte[limit];
    System.arraycopy(value, 0, multiple, 0, len);
    int copied = len;
    for (; copied < limit - copied; copied <<= 1) {
        System.arraycopy(multiple, 0, multiple, copied, copied);
    }
    System.arraycopy(multiple, 0, multiple, copied, limit - copied);
    return new String(multiple, coder);

Hol található a Stringrepeat tényleges megvalósítása? Mielőtt a tényleges kódra ugrunk, tegyünk egy rövid összefoglalót, mi az a System.arraycopy? Ez az ismétlés kód egyik fő építőköve, ezért tudnunk kell. Ismét van tömbműveletünk. Ezúttal a Array töredékét másoljuk át egyik helyről a másikra.

char[] a = "abcdefghi".toCharArray();
char[] b = "123456789".toCharArray();
System.arraycopy(a, 2, b, 4, 3);
System.out.println(a); // abcdefghi
System.out.println(b); // 1234cde89

Nézzük meg a System.arraycopy forráskódját. Nem ad sok információt, mivel ez egy Java natív módszer. Ez azt jelenti, hogy a JVM és az operációs rendszer hatékonyan tudja támogatni futás közben.

/**
 * Copies an array from the specified source array, beginning at the
 * specified position, to the specified position of the destination array.
...
 * @param      src      the source array.
 * @param      srcPos   starting position in the source array.
 * @param      dest     the destination array.
 * @param      destPos  starting position in the destination data.
 * @param      length   the number of array elements to be copied.
...
 */
@HotSpotIntrinsicCandidate
public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);

A fő kód

Először is helyet csinálunk a kimeneti karakterláncnak:

final int limit = len * count;
final byte[] multiple = new byte[limit];

Ezután az eredeti karakterlánc átkerül az új tömbbe (az első előfordulás):

System.arraycopy(value, 0, multiple, 0, len);

Ezután van egy hurok, amely 2 trükköt hajt végre.

int copied = len;
for (; copied < limit - copied; copied <<= 1) {
    System.arraycopy(multiple, 0, multiple, copied, copied);
}

Csak a céltömb kerül felhasználásra, és minden ciklus kétszer másol minden iteráció során. Az első ciklus után két előfordulásunk van, majd négy, nyolc, tizenhat stb.

1: copycopy............................
2: copycopycopycopy....................
3: copycopycopycopycopycopycopycopy....
4: ?

Minden iteráció gyorsabb lesz a másolás során, mivel a másolási művelet a nagyobb darabokhoz lesz optimalizálva. Természetesen egy bizonyos ponton nem tudjuk megkettőzni az adatokat, mivel nincs elég hely a tömbben. Az utolsó lépés a fennmaradó hely kitöltése és az eredmény létrehozása:

System.arraycopy(multiple, 0, multiple, copied, limit - copied);
return new String(multiple, coder);

A végén megvan az ismétlődő karakterláncunk.

3: copycopycopycopycopycopycopycopy....
4: copycopycopycopycopycopycopycopycopy

Mielőtt befejeznénk

Nézzük meg a módszer teljes forráskódját.

/**
 * Returns a string whose value is the concatenation of this
 * string repeated {@code count} times.
 * <p>
 * If this string is empty or count is zero then the empty
 * string is returned.
 *
 * @param   count number of times to repeat
 *
 * @return  A string composed of this string repeated
 *          {@code count} times or the empty string if this
 *          string is empty or count is zero
 *
 * @throws  IllegalArgumentException if the {@code count} is
 *          negative.
 *
 * @since 11
 */
public String repeat(int count) {
    if (count < 0) {
        throw new IllegalArgumentException("count is negative: " + count);
    }
    if (count == 1) {
        return this;
    }
    final int len = value.length;
    if (len == 0 || count == 0) {
        return "";
    }
    if (len == 1) {
        final byte[] single = new byte[count];
        Arrays.fill(single, value[0]);
        return new String(single, coder);
    }
    if (Integer.MAX_VALUE / count < len) {
        throw new OutOfMemoryError("Repeating " + len + " bytes String " + count +
                " times will produce a String exceeding maximum size.");
    }
    final int limit = len * count;
    final byte[] multiple = new byte[limit];
    System.arraycopy(value, 0, multiple, 0, len);
    int copied = len;
    for (; copied < limit - copied; copied <<= 1) {
        System.arraycopy(multiple, 0, multiple, copied, copied);
    }
    System.arraycopy(multiple, 0, multiple, copied, limit - copied);
    return new String(multiple, coder);
}

Ne csináld ezt otthon!

Egy átlagos rendszerben ilyen kódot írni olyan, mintha egy F1-es autóval hajtanák a gyerekeket iskolába – indokolatlan őrület. De a JDK fejlesztői szemszögéből ez más. A kód segítségével ténylegesen megépíthetjük az F1-es autó egyes részeit.