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;
Isaac
  • 625
  • 1
  • 12
  • 30
  • 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`. – Trevor Freeman Jul 18 '13 at 22:17
  • Thanks. Could you explain why using StringBuilder is a better idea? – Isaac Jul 18 '13 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. – Trevor Freeman Jul 18 '13 at 22:21

7 Answers7

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
durron597
  • 31,968
  • 17
  • 99
  • 158
  • 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. Jul 18 '13 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 `String`s are immutable, it can't just provide the underlying array) and `charAt` does not. – durron597 Jul 18 '13 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. Jul 19 '13 at 06: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 Jul 19 '13 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. Jul 19 '13 at 13:18
2

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

Joey
  • 344,408
  • 85
  • 689
  • 683
1

You could use this regex equivalent

String newString = str.replaceAll("(?<!^).(.)", "$1");
Reimeus
  • 158,255
  • 15
  • 216
  • 276
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.

f1sh
  • 11,489
  • 3
  • 25
  • 51
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.

Bobbick
  • 255
  • 3
  • 10
  • 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. – Trevor Freeman Jul 18 '13 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.

Eli Rose
  • 6,788
  • 8
  • 35
  • 55
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();
    }
}
Daniel Kaplan
  • 62,768
  • 50
  • 234
  • 356