0

Please bear with me. I haven't programmed in over a year and I'm currently reviewing my Java for job interviews by doing "homework questions." My function is supposed to return a string containing every second character in the given string. Is there a less awkward way to do this?

public String stringBits(String str) {
  StringBuffer tmp = new StringBuffer();
  for(int i = 0; i<str.length(); i+=2)
    tmp.append(str.charAt(i));
  String ret = new String(tmp);
  return ret;
3
  • 1
    Are you supposed to include the first character? Only fixes that leap out are that you should do return tmp.toString() instead of doing new String(tmp), and use StringBuilder instead of StringBuffer. Commented Jul 18, 2013 at 22:17
  • Thanks. Could you explain why using StringBuilder is a better idea?
    – Isaac
    Commented Jul 18, 2013 at 22:20
  • 1
    StringBuffer is synchronized whereas StringBuilder is not, so for single threaded code, it is more efficient to use StringBuilder. In fact, it is almost always better to use StringBuilder now since you would rarely be sharing an instance of such a class between threads. Commented Jul 18, 2013 at 22:21

7 Answers 7

3

I would use StringBuilder, not StringBuffer. StringBuffer is for multithreaded situations, and is therefore slower than StringBuilder, as it doesn't synchronize. I tested the four basic ways of doing this listed by various answers in this thread. Notice, though, the certain things I always do here; these should be the things your interviewer is really looking for:

  • I never use String += nextCharacter; as it is much, much slower than using a StringBuilder.
  • I set the initialCapacity because doing that is always faster. If you don't, if the StringBuilder gets full, it has to reallocate a new array and copy over, which is slow.

And the code:

import com.google.caliper.Runner;
import com.google.caliper.SimpleBenchmark;

import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.Random;

public class EveryOtherTest {
    public static class StringBenchmark extends SimpleBenchmark {
        private String input;

        protected void setUp() {
            Random r = new Random();
            int length = r.nextInt(1000) + 1000;
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < length; i++) {
                sb.append((char) ('A' + r.nextInt(26)));
            }
            input = sb.toString();
        }

        public String timeCharArrayForeach(int reps) {
            String output = "";
            Random r = new Random();
            for (int i = 0; i < reps; i++) {
                StringBuilder sb = new StringBuilder(input.length() / 2 + 1);
                boolean use = false;
                for (char c : input.toCharArray()) {
                    if(use) sb.append(c);
                    use = !use;
                }
                String newOutput = sb.toString();
                if (r.nextBoolean()) output = newOutput; // Trick the JIT
            }

            return output;
        }

        public String timeCharArrayPlusTwo(int reps) {
            String output = "";
            Random r = new Random();
            for (int i = 0; i < reps; i++) {
                StringBuilder sb = new StringBuilder(input.length() / 2 + 1);
                char[] charArray = input.toCharArray();
                for(int j = 0; j < input.length(); j += 2) {
                    sb.append(charArray[j]);
                }
                String newOutput = sb.toString();
                if (r.nextBoolean()) output = newOutput; // Trick the JIT
            }

            return output;
        }

        public String timeCharAt(int reps) {
            String output = "";
            Random r = new Random();
            for (int i = 0; i < reps; i++) {
                StringBuilder tmp = new StringBuilder(input.length() / 2 + 1);
                for (int j = 0; j < input.length(); j += 2) {
                    tmp.append(input.charAt(j));
                }
                String newOutput = tmp.toString();
                if (r.nextBoolean()) output = newOutput; // Trick the JIT
            }

            return output;
        }

        public String timeIterator(int reps) {
            String output = "";
            Random r  = new Random();
            for(int i = 0; i < reps; i++) {
                StringBuilder buf = new StringBuilder(input.length() / 2 + 1);
                StringCharacterIterator iterator = new StringCharacterIterator(input);
                for (char c = iterator.first(); c != CharacterIterator.DONE; c = iterator.next()) {
                    buf.append(c);
                    iterator.next();
                }
                String newOutput = buf.toString();
                if (r.nextBoolean()) output = newOutput; // Trick the JIT
            }

            return output;
        }

        public String timeRegex(int reps) {
            String output = "";
            Random r  = new Random();
            for(int i = 0; i < reps; i++) {
                String newOutput = input.replaceAll("(?<!^).(.)", "$1");
                if (r.nextBoolean()) output = newOutput; // Trick the JIT
            }

            return output;
        }
    }

    public static void main(String... args) {
        Runner.main(StringBenchmark.class, args);
    }
}

Results:

 0% Scenario{vm=java, trial=0, benchmark=CharArrayForeach} 2805.55 ns; ?=688.96 ns @ 10 trials
20% Scenario{vm=java, trial=0, benchmark=CharArrayPlusTwo} 3428.48 ns; ?=475.32 ns @ 10 trials
40% Scenario{vm=java, trial=0, benchmark=CharAt} 2138.68 ns; ?=379.44 ns @ 10 trials
60% Scenario{vm=java, trial=0, benchmark=Iterator} 3963.94 ns; ?=389.53 ns @ 10 trials
80% Scenario{vm=java, trial=0, benchmark=Regex} 58743.66 ns; ?=10850.33 ns @ 10 trials

       benchmark    us linear runtime
CharArrayForeach  2.81 =
CharArrayPlusTwo  3.43 =
          CharAt  2.14 =
        Iterator  3.96 ==
           Regex 58.74 ==============================

vm: java
trial: 0
5
  • Its rather unexpected that CharArray and CharAt have such a difference. What are the times for CharArray if you use a loop with index i.e. char[] ca = input.toCharArray(); for (int i = 0; ... i +=2) instead of for (char c : input.toCharArray())?
    – c.s.
    Commented Jul 18, 2013 at 22:57
  • @c.s. I'm not sure, but I expect it would be pretty similar. I think the reason is that .toCharArray() has to make an array copy (because Strings are immutable, it can't just provide the underlying array) and charAt does not.
    – durron597
    Commented Jul 18, 2013 at 23:10
  • Can you please modify your test as I suggest and re-check? As it is now, the tests are not equivelant. In CharArray the for-loop implicitly use an Iteratable this is why the results are similar to the Iterator case. If you access the array with an index and use i += 2 you would only access n/2 elements of the array and I expect it to be faster similar to the CharAt case
    – c.s.
    Commented Jul 19, 2013 at 6:03
  • @c.s. I updated the test; it's a lot slower that way. I think the reason is that array bounds are checked in Java, so not only does it copy the array, but then it does the array bounds check every time.
    – durron597
    Commented Jul 19, 2013 at 12:51
  • Thanks for the feedback. This is really unexpected. The charAt() needs to perform the same array indexes check since it uses a char[] internally. For the copy of the array though you have a point. That may be causing the delay.
    – c.s.
    Commented Jul 19, 2013 at 13:18
2

There is a StringCharacterIterator class if you prefer an iterator approach.

1

You could use this regex equivalent

String newString = str.replaceAll("(?<!^).(.)", "$1");
1
  • Also by far the slowest. See my answer :)
    – durron597
    Commented Jul 18, 2013 at 22:43
0

What you're doing works as far as I can tell. Here's a simple test case:

package com.sandbox;

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class SandboxTest {

    @Test
    public void testMocking() {
        assertEquals("foo", stringBits("f1o2o3"));
    }

    public String stringBits(String str) {
        StringBuffer tmp = new StringBuffer();
        for (int i = 0; i < str.length(); i += 2) {
            tmp.append(str.charAt(i));
        }
        String ret = new String(tmp);
        return ret;
    }
}

I think that's a pretty straight forward way of doing it. There's probably a way to do it with a regex and a group, but I have a feeling your current code would be easier to read.


I'd never heard of the StringCharacterIterator until I saw @Joey's answer, but it looks like an interesting solution. Here's the code using his answer:

package com.sandbox;

import org.junit.Test;

import java.text.CharacterIterator;
import java.text.StringCharacterIterator;

import static org.junit.Assert.assertEquals;

public class SandboxTest {

    @Test
    public void testMocking() {
        assertEquals("foo", stringBits("f1o2o3"));
    }

    public String stringBits(String str) {
        StringBuilder buf = new StringBuilder();
        StringCharacterIterator iterator = new StringCharacterIterator(str);
        for (char c = iterator.first(); c != CharacterIterator.DONE; c = iterator.next()) {
            buf.append(c);
            iterator.next();
        }
        return buf.toString();
    }
}
0

No. And this is not awkward at all. For every useful task, there might be a more suitable way, but in this case you have no choice but to iterate over the string.

0

I believe this should work too, and looks more simple to me.

public String stringBits(String str) {
    String tmp = "";
    for(int i = 0; i<str.length(); i+=2)
        tmp+=str.charAt(i);
    return tmp;

I edit to say that i should be equal to 1 if you want the second, fourth, six, ... characters.

1
  • 1
    Using a StringBuffer / StringBuilder is much better than using String concatenation since using += and a String will generate many needless inbetween objects and is very very inefficient for large strings. Not going to downvote, but this is not a particularly good solution. Commented Jul 18, 2013 at 22:24
0

You could turn the string into a CharArray and use a for-each loop:

for (char c: str.toCharArray()){
}

Of course then you would probably need a counter or a flag in order to get every other character, so it's probably not less awkward.

Not the answer you're looking for? Browse other questions tagged or ask your own question.