code: @NotNull
      public static Collector<CharSequence, ?, String> joining(
              @NotNull final CharSequence delimiter,
              @NotNull final CharSequence prefix,
              @NotNull final CharSequence suffix,
              @NotNull final String emptyValue) {
          return new CollectorsImpl<CharSequence, StringBuilder, String>(
  
                  new Supplier<StringBuilder>() {
                      @NotNull
                      @Override
                      public StringBuilder get() {
                          return new StringBuilder();
                      }
                  },
  
                  new BiConsumer<StringBuilder, CharSequence>() {
                      @Override
                      public void accept(@NotNull StringBuilder t, CharSequence u) {
                          if (t.length() > 0) {
                              t.append(delimiter);
                          } else {
                              t.append(prefix);
                          }
                          t.append(u);
                      }
                  },
  
                  new Function<StringBuilder, String>() {
                      @NotNull
                      @Override
                      public String apply(@NotNull StringBuilder value) {
                          if (value.length() == 0) {
                              return emptyValue;
                          } else {
                              value.append(suffix);
                              return value.toString();
                          }
                      }
                  }
          );
      }
docs: Returns a {@code Collector} that concatenates input elements into new string.
  
  @param delimiter  the delimiter between each element
  @param prefix  the prefix of result
  @param suffix  the suffix of result
  @param emptyValue  the string which replaces empty element if exists
  @return a {@code Collector}