Apa cara terbaik untuk membangun serangkaian item terbatas di Jawa?

317

Saat bekerja di aplikasi Java, saya baru-baru ini perlu menyusun daftar nilai yang dipisahkan koma untuk dilewatkan ke layanan web lain tanpa mengetahui berapa banyak elemen yang akan ada di muka. Yang terbaik yang bisa saya dapatkan dari atas kepala saya adalah sesuatu seperti ini:

public String appendWithDelimiter( String original, String addition, String delimiter ) {
    if ( original.equals( "" ) ) {
        return addition;
    } else {
        return original + delimiter + addition;
    }
}

String parameterString = "";
if ( condition ) parameterString = appendWithDelimiter( parameterString, "elementName", "," );
if ( anotherCondition ) parameterString = appendWithDelimiter( parameterString, "anotherElementName", "," );

Saya menyadari ini tidak terlalu efisien, karena ada string yang dibuat di semua tempat, tapi saya akan mencari kejelasan lebih dari optimasi.

Di Ruby, saya bisa melakukan sesuatu seperti ini, yang terasa jauh lebih elegan:

parameterArray = [];
parameterArray << "elementName" if condition;
parameterArray << "anotherElementName" if anotherCondition;
parameterString = parameterArray.join(",");

Tetapi karena Java tidak memiliki perintah join, saya tidak dapat menemukan yang setara.

Jadi, apa cara terbaik untuk melakukan ini di Jawa?

Sean McMains
sumber
The StringbBilder adalah cara untuk pergi - java.lang.StringBuilder.
Yusubov
Untuk Java 8 lihat jawaban ini: stackoverflow.com/a/22577623/1115554
micha

Jawaban:

542

Pra Jawa 8:

Apache commons lang adalah teman Anda di sini - ini menyediakan metode bergabung yang sangat mirip dengan yang Anda lihat di Ruby:

StringUtils.join(java.lang.Iterable,char)


Java 8:

Java 8 menyediakan bergabung keluar dari kotak melalui StringJoiner dan String.join(). Cuplikan di bawah ini menunjukkan bagaimana Anda dapat menggunakannya:

StringJoiner

StringJoiner joiner = new StringJoiner(",");
joiner.add("01").add("02").add("03");
String joinedString = joiner.toString(); // "01,02,03"

String.join(CharSequence delimiter, CharSequence... elements))

String joinedString = String.join(" - ", "04", "05", "06"); // "04 - 05 - 06"

String.join(CharSequence delimiter, Iterable<? extends CharSequence> elements)

List<String> strings = new LinkedList<>();
strings.add("Java");strings.add("is");
strings.add("cool");
String message = String.join(" ", strings);
//message returned is: "Java is cool"
Martin Gladdish
sumber
2
Saya bertanya-tanya - apakah ini memperhitungkan jika representasi String dari Objek dalam Koleksi berisi karakter pembatas itu sendiri?
GreenieMeanie
4
Persis apa yang saya cari: StringUtils.join (java.util.Collection, String) dari paket org.apache.commons.lang3.StringUtils, file jar adalah commons-lang3-3.0.1.jar
Umar
108
Di Android Anda dapat menggunakan TextUtils.join () juga.
James Wald
3
Itu bukan cara terbaik. Itu akan selalu menambahkan char bahkan jika Objek dalam Koleksi adalah null / kosong. Ini bagus dan bersih tetapi kadang-kadang akan mencetak ar pembatas ganda.
Stephan Schielke
52

Anda bisa menulis sedikit metode utilitas gaya gabungan yang berfungsi di java.util.Lists

public static String join(List<String> list, String delim) {

    StringBuilder sb = new StringBuilder();

    String loopDelim = "";

    for(String s : list) {

        sb.append(loopDelim);
        sb.append(s);            

        loopDelim = delim;
    }

    return sb.toString();
}

Kemudian gunakan seperti ini:

    List<String> list = new ArrayList<String>();

    if( condition )        list.add("elementName");
    if( anotherCondition ) list.add("anotherElementName");

    join(list, ",");
Rob Dickerson
sumber
2
mengapa Anda harus menulis metode Anda sendiri jika setidaknya 2 implementasi (apache dan jambu) sudah ada?
Timofey
29
Ini mungkin, misalnya, berguna jika seseorang ingin memiliki lebih sedikit ketergantungan eksternal.
Thomas Zumbrunn
2
Saya agak suka bagaimana loopDelim digunakan daripada kondisi. Ini semacam peretasan, tetapi menghapus pernyataan bersyarat dari suatu siklus. Saya masih lebih suka menggunakan iterator dan menambahkan elemen pertama sebelum siklus, tapi ini memang hack yang bagus.
Vlasec
Tidak bisakah Anda mengubah List<String>intitializer Anda ke Iterator<?>dan memiliki efek yang sama?
k_g
@Tim Misalnya apache tidak melewatkan string kosong.
banterCZ
31

The perpustakaan Jambu Google memiliki com.google.common.base.Joiner kelas yang membantu untuk menyelesaikan tugas-tugas tersebut.

Sampel:

"My pets are: " + Joiner.on(", ").join(Arrays.asList("rabbit", "parrot", "dog")); 
// returns "My pets are: rabbit, parrot, dog"

Joiner.on(" AND ").join(Arrays.asList("field1=1" , "field2=2", "field3=3"));
// returns "field1=1 AND field2=2 AND field3=3"

Joiner.on(",").skipNulls().join(Arrays.asList("London", "Moscow", null, "New York", null, "Paris"));
// returns "London,Moscow,New York,Paris"

Joiner.on(", ").useForNull("Team held a draw").join(Arrays.asList("FC Barcelona", "FC Bayern", null, null, "Chelsea FC", "AC Milan"));
// returns "FC Barcelona, FC Bayern, Team held a draw, Team held a draw, Chelsea FC, AC Milan"

Berikut adalah artikel tentang utilitas string Guava .

Alex K
sumber
26

Di Java 8 Anda dapat menggunakan String.join():

List<String> list = Arrays.asList("foo", "bar", "baz");
String joined = String.join(" and ", list); // "foo and bar and baz"

Lihat juga jawaban ini untuk contoh API Aliran.

micha
sumber
20

Anda dapat menggeneralisasikannya, tetapi tidak ada yang bergabung di Jawa, seperti yang Anda katakan.

Ini mungkin bekerja lebih baik.

public static String join(Iterable<? extends CharSequence> s, String delimiter) {
    Iterator<? extends CharSequence> iter = s.iterator();
    if (!iter.hasNext()) return "";
    StringBuilder buffer = new StringBuilder(iter.next());
    while (iter.hasNext()) buffer.append(delimiter).append(iter.next());
    return buffer.toString();
}
Vinko Vrsalovic
sumber
Saya setuju dengan jawaban ini tetapi dapatkah seseorang mengedit tanda tangan sehingga menerima Koleksi <String> bukan AbstractCollection <String>? Sisa kode harus sama tetapi saya pikir AbstractCollection adalah detail implementasi yang tidak masalah di sini.
Outlaw Programmer
Lebih baik lagi, gunakan Iterable<String>dan gunakan saja fakta bahwa Anda dapat mengulanginya. Dalam contoh Anda, Anda tidak perlu jumlah item dalam koleksi, jadi ini bahkan lebih umum.
Jason Cohen
1
Atau bahkan lebih baik, gunakan Iterable <? memperluas Charsequence> dan kemudian Anda dapat menerima koleksi StringBuilders dan Strings dan stream dan hal-hal seperti string lainnya. :)
Jason Cohen
2
Anda ingin menggunakan StringBuildergantinya. Mereka identik, kecuali StringBuffermemberikan keamanan utas yang tidak perlu. Bisakah seseorang mengeditnya!
Casebash
1
Kode ini memiliki beberapa kesalahan. 1. CharSequence memiliki modal s. 2. s.iterator () mengembalikan Iterator <? memperluas CharSequence>. 3. Iterable tidak memiliki isEmpty()metode, gunakan next()metode ini
Casebash
17

di Java 8 Anda dapat melakukan ini seperti:

list.stream().map(Object::toString)
        .collect(Collectors.joining(delimiter));

jika daftar memiliki nol Anda dapat menggunakan:

list.stream().map(String::valueOf)
        .collect(Collectors.joining(delimiter))

itu juga mendukung awalan dan sufiks:

list.stream().map(String::valueOf)
        .collect(Collectors.joining(delimiter, prefix, suffix));
gladiator
sumber
15

Gunakan pendekatan berdasarkan java.lang.StringBuilder! ("Urutan karakter yang bisa berubah.")

Seperti yang Anda sebutkan, semua rangkaian string itu menciptakan String di seluruh. StringBuildertidak akan melakukan itu.

Kenapa StringBuilderbukannya StringBuffer? DariStringBuilder javadoc:

Jika memungkinkan, disarankan agar kelas ini digunakan dalam preferensi untuk StringBuffer karena akan lebih cepat di sebagian besar implementasi.

Stu Thompson
sumber
4
Ya. Selain itu, StringBuffer aman untuk thread, sedangkan StringBuilder tidak.
Jon Onstott
10

Saya akan menggunakan Google Collections. Ada fasilitas Gabung yang bagus.
http://google-collections.googlecode.com/svn/trunk/javadoc/index.html?com/google/common/base/Join.html

Tetapi jika saya ingin menulisnya sendiri,

package util;

import java.util.ArrayList;
import java.util.Iterable;
import java.util.Collections;
import java.util.Iterator;

public class Utils {
    // accept a collection of objects, since all objects have toString()
    public static String join(String delimiter, Iterable<? extends Object> objs) {
        if (objs.isEmpty()) {
            return "";
        }
        Iterator<? extends Object> iter = objs.iterator();
        StringBuilder buffer = new StringBuilder();
        buffer.append(iter.next());
        while (iter.hasNext()) {
            buffer.append(delimiter).append(iter.next());
        }
        return buffer.toString();
    }

    // for convenience
    public static String join(String delimiter, Object... objs) {
        ArrayList<Object> list = new ArrayList<Object>();
        Collections.addAll(list, objs);
        return join(delimiter, list);
    }
}

Saya pikir ini berfungsi lebih baik dengan koleksi objek, karena sekarang Anda tidak perlu mengubah objek Anda menjadi string sebelum Anda bergabung dengan mereka.

Eric Normand
sumber
8

Kelas Apache commons StringUtils memiliki metode bergabung.


sumber
5

Java 8

stringCollection.stream().collect(Collectors.joining(", "));
gstackoverflow
sumber
3

Anda dapat menggunakan StringBuildertipe Java untuk ini. Ada juga StringBuffer, tetapi ini berisi logika keselamatan ulir ekstra yang seringkali tidak perlu.

Kent Boogaart
sumber
3

Dan yang minimal (jika Anda tidak ingin memasukkan Apache Commons atau Gauva ke dalam dependensi proyek hanya untuk bergabung dengan string)

/**
 *
 * @param delim : String that should be kept in between the parts
 * @param parts : parts that needs to be joined
 * @return  a String that's formed by joining the parts
 */
private static final String join(String delim, String... parts) {
    StringBuilder builder = new StringBuilder();
    for (int i = 0; i < parts.length - 1; i++) {
        builder.append(parts[i]).append(delim);
    }
    if(parts.length > 0){
        builder.append(parts[parts.length - 1]);
    }
    return builder.toString();
}
Thamme Gowda
sumber
Gunakan delim sebagai ganti File.separator.
Pradeep Kumar
3

Gunakan StringBuilder dan kelas Separator

StringBuilder buf = new StringBuilder();
Separator sep = new Separator(", ");
for (String each : list) {
    buf.append(sep).append(each);
}

Pemisah membungkus pembatas. Pembatas dikembalikan dengan toStringmetode Separator , kecuali pada panggilan pertama yang mengembalikan string kosong!

Kode sumber untuk kelas Separator

public class Separator {

    private boolean skipFirst;
    private final String value;

    public Separator() {
        this(", ");
    }

    public Separator(String value) {
        this.value = value;
        this.skipFirst = true;
    }

    public void reset() {
        skipFirst = true;
    }

    public String toString() {
        String sep = skipFirst ? "" : value;
        skipFirst = false;
        return sep;
    }

}
akuhn
sumber
Bagaimana dengan memodifikasi Separatorsehingga menggunakan StringBuilderalih - alih penggabungan Strings?
Mohamed Nuur
@Mohamed Separatorbaru saja mengembalikan pembatas, itu tidak menyatukan string itu sendiri.
akuhn
Kelas apa ini Separator??? Kenapa tidak menggunakan String sederhana saja ..?!
maxxyme
2

Mengapa tidak menulis metode join () Anda sendiri? Itu akan mengambil sebagai parameter koleksi String dan String pembatas. Dalam metode iterate atas koleksi dan membangun hasil Anda di StringBuffer.

Dave Costa
sumber
2

Jika Anda menggunakan Spring MVC maka Anda dapat mencoba langkah-langkah berikut.

import org.springframework.util.StringUtils;

List<String> groupIds = new List<String>;   
groupIds.add("a");    
groupIds.add("b");    
groupIds.add("c");

String csv = StringUtils.arrayToCommaDelimitedString(groupIds.toArray());

Itu akan menghasilkan a,b,c

Ankit Lalan
sumber
2

Jika Anda menggunakan Eclipse Collections , Anda dapat menggunakan makeString()atau appendString().

makeString()mengembalikan Stringrepresentasi, mirip dengantoString() .

Itu memiliki tiga bentuk

  • makeString(start, separator, end)
  • makeString(separator) default dimulai dan diakhiri dengan string kosong
  • makeString()default pemisah untuk ", "(koma dan spasi)

Contoh kode:

MutableList<Integer> list = FastList.newListWith(1, 2, 3);
assertEquals("[1/2/3]", list.makeString("[", "/", "]"));
assertEquals("1/2/3", list.makeString("/"));
assertEquals("1, 2, 3", list.makeString());
assertEquals(list.toString(), list.makeString("[", ", ", "]"));

appendString()mirip dengan makeString(), tetapi ditambahkan ke Appendable(suka StringBuilder) dan void. Ini memiliki tiga bentuk yang sama, dengan argumen pertama tambahan, Appendable.

MutableList<Integer> list = FastList.newListWith(1, 2, 3);
Appendable appendable = new StringBuilder();
list.appendString(appendable, "[", "/", "]");
assertEquals("[1/2/3]", appendable.toString());

Jika Anda tidak dapat mengonversi koleksi Anda ke jenis Eclipse Collections, cukup sesuaikan dengan adaptor yang relevan.

List<Object> list = ...;
ListAdapter.adapt(list).makeString(",");

Catatan: Saya pengendara untuk koleksi Eclipse.

Craig P. Motlin
sumber
2

Java 8 Jenis Asli

List<Integer> example;
example.add(1);
example.add(2);
example.add(3);
...
example.stream().collect(Collectors.joining(","));

Java 8 Custom Object:

List<Person> person;
...
person.stream().map(Person::getAge).collect(Collectors.joining(","));
Luis Aguilar
sumber
1

Anda mungkin harus menggunakan a StringBuilderdengan appendmetode untuk membangun hasil Anda, tetapi jika tidak, ini adalah solusi yang bagus seperti yang ditawarkan Java.

hari baru tiba
sumber
1

Mengapa Anda tidak melakukan di Jawa hal yang sama dengan yang Anda lakukan di ruby, yaitu membuat string pembatas dipisahkan hanya setelah Anda menambahkan semua bagian ke array?

ArrayList<String> parms = new ArrayList<String>();
if (someCondition) parms.add("someString");
if (anotherCondition) parms.add("someOtherString");
// ...
String sep = ""; StringBuffer b = new StringBuffer();
for (String p: parms) {
    b.append(sep);
    b.append(p);
    sep = "yourDelimiter";
}

Anda mungkin ingin memindahkannya untuk loop dalam metode helper terpisah, dan juga menggunakan StringBuilder alih-alih StringBuffer ...

Sunting : memperbaiki urutan penambahan.

agnul
sumber
Ya, mengapa Anda menggunakan StringBuffer alih-alih StringBuilder (karena Anda menggunakan Java 1.5+)? Anda juga salah menambahkan.
Tom Hawtin - tackline
1

Dengan Java 5 variabel args, jadi Anda tidak perlu memasukkan semua string Anda ke dalam koleksi atau array secara eksplisit:

import junit.framework.Assert;
import org.junit.Test;

public class StringUtil
{
    public static String join(String delim, String... strings)
    {
        StringBuilder builder = new StringBuilder();

        if (strings != null)
        {
            for (String str : strings)
            {
                if (builder.length() > 0)
                {
                    builder.append(delim).append(" ");
                }
                builder.append(str);
            }
        }           
        return builder.toString();
    }
    @Test
    public void joinTest()
    {
        Assert.assertEquals("", StringUtil.join(",", null));
        Assert.assertEquals("", StringUtil.join(",", ""));
        Assert.assertEquals("", StringUtil.join(",", new String[0]));
        Assert.assertEquals("test", StringUtil.join(",", "test"));
        Assert.assertEquals("foo, bar", StringUtil.join(",", "foo", "bar"));
        Assert.assertEquals("foo, bar, x", StringUtil.join(",", "foo", "bar", "x"));
    }
}
Mark B
sumber
1

Bagi mereka yang berada dalam konteks Musim Semi kelas StringUtils mereka berguna juga:

Ada banyak cara pintas yang bermanfaat seperti:

  • collectionToCommaDelimitedString (Koleksi coll)
  • collectionToDelimitedString (Koleksi coll, String delim)
  • arrayToDelimitedString (Object [] arr, String delim)

dan banyak lagi.

Ini bisa membantu jika Anda belum menggunakan Java 8 dan Anda sudah berada dalam konteks Musim Semi.

Saya lebih suka melawan Apache Commons (walaupun sangat bagus juga) untuk dukungan Koleksi yang lebih mudah seperti ini:

// Encoding Set<String> to String delimited 
String asString = org.springframework.util.StringUtils.collectionToDelimitedString(codes, ";");

// Decoding String delimited to Set
Set<String> collection = org.springframework.util.StringUtils.commaDelimitedListToSet(asString);
рüффп
sumber
0

Anda dapat mencoba sesuatu seperti ini:

StringBuilder sb = new StringBuilder();
if (condition) { sb.append("elementName").append(","); }
if (anotherCondition) { sb.append("anotherElementName").append(","); }
String parameterString = sb.toString();
martinatime
sumber
Sepertinya ini akan meninggalkan koma liar di ujung senar, yang saya harap akan hindari. (Tentu saja, karena Anda tahu itu ada di sana, Anda kemudian dapat memotongnya, tetapi baunya juga agak tidak enak.)
Sean McMains
0

Jadi pada dasarnya sesuatu seperti ini:

public static String appendWithDelimiter(String original, String addition, String delimiter) {

if (original.equals("")) {
    return addition;
} else {
    StringBuilder sb = new StringBuilder(original.length() + addition.length() + delimiter.length());
        sb.append(original);
        sb.append(delimiter);
        sb.append(addition);
        return sb.toString();
    }
}
killdash10
sumber
Masalahnya di sini adalah bahwa ia tampaknya memanggil appendWithDelimiter () allot. Solusinya harus instance satu dan hanya satu StringBuffer dan bekerja dengan instance tunggal itu.
Stu Thompson
0

Tidak tahu apakah ini benar-benar lebih baik, tapi setidaknya itu menggunakan StringBuilder, yang mungkin sedikit lebih efisien.

Di bawah ini adalah pendekatan yang lebih umum jika Anda dapat membangun daftar parameter SEBELUM melakukan pembatasan parameter apa pun.

// Answers real question
public String appendWithDelimiters(String delimiter, String original, String addition) {
    StringBuilder sb = new StringBuilder(original);
    if(sb.length()!=0) {
        sb.append(delimiter).append(addition);
    } else {
        sb.append(addition);
    }
    return sb.toString();
}


// A more generic case.
// ... means a list of indeterminate length of Strings.
public String appendWithDelimitersGeneric(String delimiter, String... strings) {
    StringBuilder sb = new StringBuilder();
    for (String string : strings) {
        if(sb.length()!=0) {
            sb.append(delimiter).append(string);
        } else {
            sb.append(string);
        }
    }

    return sb.toString();
}

public void testAppendWithDelimiters() {
    String string = appendWithDelimitersGeneric(",", "string1", "string2", "string3");
}
Mikezx6r
sumber
0

Pendekatan Anda tidak terlalu buruk, tetapi Anda harus menggunakan StringBuffer alih-alih menggunakan tanda +. + Memiliki kerugian besar bahwa instance String baru sedang dibuat untuk setiap operasi tunggal. Semakin lama string Anda, semakin besar biaya overhead. Jadi menggunakan StringBuffer harus menjadi cara tercepat:

public StringBuffer appendWithDelimiter( StringBuffer original, String addition, String delimiter ) {
        if ( original == null ) {
                StringBuffer buffer = new StringBuffer();
                buffer.append(addition);
                return buffer;
        } else {
                buffer.append(delimiter);
                buffer.append(addition);
                return original;
        }
}

Setelah Anda selesai membuat string, cukup panggil toString () pada StringBuffer yang dikembalikan.

Yaba
sumber
1
Menggunakan StringBuffer di sini hanya akan membuatnya lebih lambat tanpa alasan. Menggunakan operator + secara internal akan menggunakan StringBuilder yang lebih cepat sehingga ia tidak memenangkan apa pun selain keselamatan utas yang tidak ia butuhkan dengan menggunakan StringBuffer sebagai gantinya.
Fredrik
Juga ... Pernyataan "The + memiliki kerugian besar bahwa instance String baru sedang dibuat untuk setiap operasi tunggal" adalah salah. Kompiler akan menghasilkan panggilan StringBuilder.append () dari mereka.
Fredrik
0

Alih-alih menggunakan penggabungan string, Anda harus menggunakan StringBuilder jika kode Anda tidak di-thread, dan StringBuffer jika benar.

Harun
sumber
0

Anda membuat ini sedikit lebih rumit dari yang seharusnya. Mari kita mulai dengan akhir dari contoh Anda:

String parameterString = "";
if ( condition ) parameterString = appendWithDelimiter( parameterString, "elementName", "," );
if ( anotherCondition ) parameterString = appendWithDelimiter( parameterString, "anotherElementName", "," );

Dengan perubahan kecil menggunakan StringBuilder alih-alih sebuah String, ini menjadi:

StringBuilder parameterString = new StringBuilder();
if (condition) parameterString.append("elementName").append(",");
if (anotherCondition) parameterString.append("anotherElementName").append(",");
...

Setelah selesai (saya berasumsi Anda harus memeriksa beberapa kondisi lain juga), pastikan Anda menghapus koma tailing dengan perintah seperti ini:

if (parameterString.length() > 0) 
    parameterString.deleteCharAt(parameterString.length() - 1);

Dan akhirnya, dapatkan string yang Anda inginkan

parameterString.toString();

Anda juga bisa mengganti "," di panggilan kedua untuk menambahkan dengan string pembatas umum yang dapat diatur untuk apa saja. Jika Anda memiliki daftar hal-hal yang Anda tahu perlu Anda tambahkan (tanpa syarat), Anda bisa memasukkan kode ini ke dalam metode yang mengambil daftar string.


sumber
0
//Note: if you have access to Java5+, 
//use StringBuilder in preference to StringBuffer.  
//All that has to be replaced is the class name.  
//StringBuffer will work in Java 1.4, though.

appendWithDelimiter( StringBuffer buffer, String addition, 
    String delimiter ) {
    if ( buffer.length() == 0) {
        buffer.append(addition);
    } else {
        buffer.append(delimiter);
        buffer.append(addition);
    }
}


StringBuffer parameterBuffer = new StringBuffer();
if ( condition ) { 
    appendWithDelimiter(parameterBuffer, "elementName", "," );
}
if ( anotherCondition ) {
    appendWithDelimiter(parameterBuffer, "anotherElementName", "," );
}

//Finally, to return a string representation, call toString() when returning.
return parameterBuffer.toString(); 
MetroidFan2002
sumber
0

Jadi, beberapa hal yang mungkin Anda lakukan untuk merasakan bahwa sepertinya Anda sedang mencari:

1) Perluas kelas Daftar - dan tambahkan metode bergabung ke dalamnya. Metode join hanya akan melakukan pekerjaan menggabungkan dan menambahkan pembatas (yang bisa menjadi param untuk metode join)

2) Sepertinya Java 7 akan menambahkan metode ekstensi ke java - yang memungkinkan Anda hanya melampirkan metode tertentu ke kelas: sehingga Anda dapat menulis metode bergabung itu dan menambahkannya sebagai metode ekstensi ke Daftar atau bahkan untuk Koleksi.

Solusi 1 mungkin adalah satu-satunya yang realistis, sekarang, karena Java 7 belum keluar :) Tapi seharusnya berfungsi dengan baik.

Untuk menggunakan keduanya, Anda hanya perlu menambahkan semua item Anda ke Daftar atau Koleksi seperti biasa, dan kemudian memanggil metode kustom baru untuk 'bergabung' dengan mereka.

pengguna13276
sumber