Cheat sheet!
Front:
Back:
My first multi-threaded application: Original single-threaded version
This duplicates a directory structure, where the tab indentation in every text file is replaced with spaces. First, the single-threaded version (this is the same function as in my XBN-Java library, extracted so it has no dependencies on any other part of the library…aside from the timing class).
This took 2,764,557,795 nanoseconds to execute. 2.7 seconds.
package multithreading_tabs_to_spaces; import com.github.xbn.testdev.TimedTest; import java.io.BufferedWriter; import java.io.Console; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.text.DecimalFormat; import java.util.Iterator; import java.util.Objects; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <code>java multithreading_tabs_to_spaces.IndentTabsToSpaces_NonMultiThreaded 0 C:\data_jeffy\code\wordpress_posts\java\multithreading_tabs_to_spaces\xbnjava_my_private_sandbox_with_tab_indentation\</code> * * <code>java -classpath .;C:\data_jeffy\code\wordpress_posts\java\multithreading_tabs_to_spaces\jar_dependencies\commons-io-2.4.jar;C:\data_jeffy\code\wordpress_posts\java\multithreading_tabs_to_spaces\jar_dependencies\slf4j-api-1.7.12.jar;C:\data_jeffy\code\wordpress_posts\java\multithreading_tabs_to_spaces\jar_dependencies\slf4j-simple-1.7.12.jar multithreading_tabs_to_spaces.IndentTabsToSpaces_NonMultiThreaded 5 C:\data_jeffy\code\wordpress_posts\java\multithreading_tabs_to_spaces\xbnjava_my_private_sandbox_with_tab_indentation</code> */ public class IndentTabsToSpaces_NonMultiThreaded { private static final String FILE_SEP = System.getProperty("file.separator", "\\"); private static final String PARAM_REQUIREMENTS = "Two required parameters: " + "Number of y/n overwrite prompts, and the path of the directory containing " + "the files to overwrite (must *not* end with a " + FILE_SEP + "."; public static final void main(String[] cmd_lineParams) { int areYouSureOverwritePromptCount; String sourceDirPreDashSrcDst; try { areYouSureOverwritePromptCount = Integer.parseInt(cmd_lineParams[0]); //itr = FileUtils.iterateFiles(new File(cmd_lineParams[1]), FileFilterUtils.trueFileFilter(), null); sourceDirPreDashSrcDst = cmd_lineParams[1]; } catch(ArrayIndexOutOfBoundsException x) { throw new RuntimeException(PARAM_REQUIREMENTS); } catch(NumberFormatException x) { throw new RuntimeException("Parameter one must be an integer. " + PARAM_REQUIREMENTS); } String sourceDir = sourceDirPreDashSrcDst + "-source" + FILE_SEP; Iterator<File> itr = FileUtils.iterateFiles(new File(sourceDir), new String[] {"xml", "java", "txt", "bat", "md", "log", "javax"}, true); TimedTest singleThreadedTest = new TimedTest("single-threaded"); singleThreadedTest.declareStartWithOutput(); try { new TabToSpaceIndenter().replaceAllInDirectory( areYouSureOverwritePromptCount, itr, sourceDir, sourceDirPreDashSrcDst + "-destination" + FILE_SEP); } catch(IOException x) { throw new RuntimeException(x); } singleThreadedTest.declareEndWithOutput(); } } class TabToSpaceIndenter { //State: private final String rplcmntSpaces; private final Logger logger = LoggerFactory.getLogger("TabToSpaceIndenter"); //Constants: private static final DecimalFormat DEC_FMT = new DecimalFormat("#.###"); private static final String LINE_SEP = System.getProperty("line.separator", "\r\n"); private static final boolean OVERWRITE = false; private static final boolean MANUAL_FLUSH = false; /** * The default value to replace each tab with--equal to three spaces. * @see #getReplacementSpaces() */ public static final String SPACES_TO_RPLC_WITH_DEFAULT = " "; /** * <p>Create a new instance with the default spaces-replacement.</p> * * <p>Equal to * <br> <code>{@link #TabToSpaceIndenter(String) this}(SPACES_TO_RPLC_WITH_DEFAULT)</code></p> */ public TabToSpaceIndenter() { this(SPACES_TO_RPLC_WITH_DEFAULT); } /** * Create a new instance. * @param spaces_toRplcWith The spaces to replace each tab with. May * not be {@code null} or empty. Get with * {@link #getReplacementSpaces() getReplacementSpaces}{@code ()}. * @see #TabToSpaceIndenter() */ public TabToSpaceIndenter(String spaces_toRplcWith) { try { if(spaces_toRplcWith.length() == 0) { throw new IllegalArgumentException("spaces_toRplcWith has no characters."); } } catch(NullPointerException x) { Objects.requireNonNull(spaces_toRplcWith, "spaces_toRplcWith"); throw x; } rplcmntSpaces = spaces_toRplcWith; } /** * The value to replace each tab with. * @see #TabToSpaceIndenter(String) * @see #SPACES_TO_RPLC_WITH_DEFAULT */ public String getReplacementSpaces() { return rplcmntSpaces; } /** * <p>Utility function to replace all indentation tabs for all files in * a directory--<i><b>this overwrites all files!</b></i></p> * * @param areYouSure_overwritePrompts If greater than zero, this is the * number of times the user is presented with a prompt to confirm * overwriting. If five, for instance, the <i>first</i> five files are * not overwritten until the user confirms each (by answering with a * <code>'Y'</code> or <code>'y'</code>. Anything else aborts the * application. * @param file_itr May not be {@code null}. <i>Every</i> returned file * in this iterator is expected to be a plain-text file (so filter it as * necessary), and both readable and writable. * <i>Should</i> be non-{@code null} and non-empty. * @param source_baseDir The directory base-path in which files are read * from. This is what is replaced by <code>dest_baseDir</code>. May not * be {@code null} and must begin the path for every file returned by * <code>file_itr</code>. * @param dest_baseDir The directory base-path that output is written * to. This is what <code>source_baseDir</code> is replaced by. May not * be {@code null} or empty. * @see #getReplacementSpaces() * @see org.apache.commons.io.FileUtils#iterateFiles(File, IOFileFilter, IOFileFilter) commons.io.FileUtils#iterateFiles */ public void replaceAllInDirectory(int areYouSure_overwritePrompts, Iterator<File> file_itr, String source_baseDir, String dest_baseDir) throws IOException { logger.info("Source dir: " + source_baseDir); logger.info("Destination dir: " + dest_baseDir); Objects.requireNonNull(file_itr, "file_itr"); int fileCount = 0; //How many total files were analyzed (and //potentially changed)? int totalTabsRplcd = 0; //How many tabs were replaced in *all* int aysPromptsGiven = 0; //files? while(file_itr.hasNext()) { File f = file_itr.next(); fileCount++; String sourcePath = f.getAbsolutePath(); if(!sourcePath.startsWith(source_baseDir)) { throw new IllegalArgumentException("sourcePath (" + sourcePath + ") does not start with source_baseDir (" + source_baseDir + ")."); } String destPath = dest_baseDir + sourcePath.substring(source_baseDir.length()); if(++aysPromptsGiven < areYouSure_overwritePrompts) { String promptText = "[" + aysPromptsGiven + "/" + areYouSure_overwritePrompts + "] About to overwrite" + LINE_SEP + " " + destPath + LINE_SEP + "Are you sure? 'Y' or 'y' to " + "proceed. Anything else to abort: "; Console console = System.console(); Objects.requireNonNull(console, "System.console() (This error is " + "expected when you run this in Eclipse. This works when " + "executing it directly on the console. If you set the first " + "parameter to zero, you can safely run it in Eclipse.)"); String input = console.readLine(promptText); if(!input.toLowerCase().equals("y")) { System.out.println("Aborting."); return; } } //Replace the tabs in this file, and get the number of tabs //actually replaced. totalTabsRplcd += replaceAllInFile(f, destPath); } //Print summary to console. String avgTabsPerFileStr = DEC_FMT.format(totalTabsRplcd / new Double(fileCount)); logger.info("Done. {} total tabs replaced in {} total files ({}/file)", totalTabsRplcd, fileCount, avgTabsPerFileStr, logger); } public int replaceAllInFile(File source_file, String dest_path) { try { logger.trace("Getting input line iterator to {}." + source_file.getAbsolutePath()); } catch(NullPointerException x) { Objects.requireNonNull(source_file, "source_file"); Objects.requireNonNull(logger, "logger"); throw x; } Iterator<String> lineItrInput = null; try { lineItrInput = FileUtils.lineIterator(source_file); } catch(Exception x) { throw new RuntimeException( "Attempting to obtain line iterator for \"" + source_file.getAbsolutePath() + "\"", x); } logger.debug("Creating output print writer to dest_path (\"{}\").", dest_path); PrintWriter writerOut = null; try { writerOut = (new PrintWriter(new BufferedWriter( new FileWriter(dest_path, OVERWRITE)), MANUAL_FLUSH)); } catch(Exception x) { throw new RuntimeException("Attempting to create a print writer to \"" + dest_path + "\"", x); } int totalLines = 0; //How many lines in this file? int tabsRplcd = 0; //How many tabs total in *all* lines? try { while(lineItrInput.hasNext()) { String line = lineItrInput.next(); totalLines++; if(line.length() == 0) { //No text at all, so definitely no tabs. writerOut.write(LINE_SEP); logger.trace("{} No characters. Writing out LINE_SEP only.", totalLines); continue; } //At least some text. int charIdx = 0; //Starting at the left-most character, while it's a tab, //replace it with spaces. while(charIdx < line.length() && line.charAt(charIdx) == '\t') { //There *is* another character in the line, and it *is* a //tab. charIdx++; writerOut.write(getReplacementSpaces()); tabsRplcd++; logger.trace("{}. {} tabs replaced.", totalLines, tabsRplcd); } //No more tabs. Append the rest of the line. writerOut.write(line.substring(charIdx)); writerOut.write(LINE_SEP); logger.trace("{}. No more tabs in this file. Appending the rest of the line.", totalLines); } } catch(Exception x) { throw new RuntimeException("source_file=" + source_file.getAbsolutePath() + ", dest_path=" + dest_path + ""); } finally { writerOut.flush(); writerOut.close(); logger.trace("{}. Writer flushed and closed.", totalLines); } String avgTabsPerLineStr = DEC_FMT.format(tabsRplcd / new Double(totalLines)); logger.debug("This file: {} lines, {} tabs, {} average per line", totalLines, tabsRplcd, avgTabsPerLineStr); return tabsRplcd; } }
My first multi-threaded application: The same class, changed to multi-threading
This uses one of a number of executors (and how long it took):
- Single threaded (2,247,164,905 nanoseconds, 2.2 seconds)
- Fixed thread pool with 2 (958,872,242), 5 (731,709,280), 10 (699,200,429), and 20 threads (704,502,249),
- Cached thread pool (765,272,723)
So the ten-to-twenty-thread thread pool is the clear winner (the above, original single-threaded version took 2.7 seconds).
package multithreading_tabs_to_spaces; import java.io.BufferedWriter; import java.io.Console; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.Iterator; import java.util.List; import java.util.Objects; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.github.xbn.testdev.TimedTest; /** * java multithreading_tabs_to_spaces.IndentTabsToSpaces_NonMultiThreaded 0 C:\data_jeffy\code\wordpress_posts\java\multithreading_tabs_to_spaces\xbnjava_my_private_sandbox_with_tab_indentation\ * * java -classpath C:\data_jeffy\code\wordpress_posts\java\multithreading_tabs_to_spaces\bin\;C:\data_jeffy\code\wordpress_posts\java\multithreading_tabs_to_spaces\jar_dependencies\commons-io-2.4.jar;C:\data_jeffy\code\wordpress_posts\java\multithreading_tabs_to_spaces\jar_dependencies\slf4j-api-1.7.12.jar;C:\data_jeffy\code\wordpress_posts\java\multithreading_tabs_to_spaces\jar_dependencies\slf4j-simple-1.7.12.jar multithreading_tabs_to_spaces.IndentTabsToSpaces_MultiThreaded 5 C:\data_jeffy\code\wordpress_posts\java\multithreading_tabs_to_spaces\xbnjava_my_private_sandbox_with_tab_indentation */ public class IndentTabsToSpaces_MultiThreaded { private static final String FILE_SEP = System.getProperty("file.separator", "\\"); private static final String PARAM_REQUIREMENTS = "Two required parameters: " + "Number of y/n overwrite prompts, and the path of the directory containing " + "the files to overwrite (must *not* end with a " + FILE_SEP + "."; public static final void main(String[] cmd_lineParams) { int areYouSureOverwritePromptCount; String sourceDirPreDashSrcDst; try { areYouSureOverwritePromptCount = Integer.parseInt(cmd_lineParams[0]); //itr = FileUtils.iterateFiles(new File(cmd_lineParams[1]), FileFilterUtils.trueFileFilter(), null); sourceDirPreDashSrcDst = cmd_lineParams[1]; } catch(ArrayIndexOutOfBoundsException x) { throw new RuntimeException(PARAM_REQUIREMENTS); } catch(NumberFormatException x) { throw new RuntimeException("Parameter one must be an integer. " + PARAM_REQUIREMENTS); } String sourceDir = sourceDirPreDashSrcDst + "-source" + FILE_SEP; String destDir = sourceDirPreDashSrcDst + "-destination" + FILE_SEP; Iterator<File> itr = FileUtils.iterateFiles(new File(sourceDir), new String[] {"xml", "java", "txt", "bat", "md", "log", "javax"}, true); TimedTest singleThreadedExecutorTest = new TimedTest("single-threaded executor"); singleThreadedExecutorTest.declareStartWithOutput(); replaceAllInDir(itr, sourceDir, destDir, areYouSureOverwritePromptCount, Executors.newSingleThreadExecutor()); singleThreadedExecutorTest.declareEndWithOutput(); // pauseOneSecond(); // TimedTest twoThreadFixedPoolTest = new TimedTest("2 thread fixed pool"); // twoThreadFixedPoolTest.declareStartWithOutput(); // replaceAllInDir(itr, sourceDir, destDir, areYouSureOverwritePromptCount, // Executors.newFixedThreadPool(2)); // twoThreadFixedPoolTest.declareEndWithOutput(); // pauseOneSecond(); // TimedTest fiveThreadFixedPoolTest = new TimedTest("5 thread fixed pool"); // fiveThreadFixedPoolTest.declareStartWithOutput(); // replaceAllInDir(itr, sourceDir, destDir, areYouSureOverwritePromptCount, // Executors.newFixedThreadPool(5)); // fiveThreadFixedPoolTest.declareEndWithOutput(); // pauseOneSecond(); // TimedTest tenThreadFixedPoolTest = new TimedTest("10 thread fixed pool"); // tenThreadFixedPoolTest.declareStartWithOutput(); // replaceAllInDir(itr, sourceDir, destDir, areYouSureOverwritePromptCount, // Executors.newFixedThreadPool(10)); // tenThreadFixedPoolTest.declareEndWithOutput(); // pauseOneSecond(); // TimedTest tenThreadFixedPoolTest = new TimedTest("10 thread fixed pool"); // tenThreadFixedPoolTest.declareStartWithOutput(); // replaceAllInDir(itr, sourceDir, destDir, areYouSureOverwritePromptCount, // Executors.newFixedThreadPool(20)); // tenThreadFixedPoolTest.declareEndWithOutput(); // pauseOneSecond(); // TimedTest cachedPoolTest = new TimedTest("cached pool"); // cachedPoolTest.declareStartWithOutput(); // replaceAllInDir(itr, sourceDir, destDir, areYouSureOverwritePromptCount, // Executors.newCachedThreadPool()); // cachedPoolTest.declareEndWithOutput(); } private static final void pauseOneSecond() { try { Thread.sleep(1000); } catch(InterruptedException ex) { Thread.currentThread().interrupt(); } } private static final void replaceAllInDir(Iterator<File> itr, String source_dir, String dest_dir, int ays_promptCount, ExecutorService exec_svc) { try { new TabToSpaceIndenterMT().replaceAllInDirectory(ays_promptCount, itr, source_dir, dest_dir, exec_svc); } catch(IOException | InterruptedException x) { throw new RuntimeException(x); } } } class TabToSpaceIndenterMT { //State: private final String rplcmntSpaces; private final Logger logger = LoggerFactory.getLogger("TabToSpaceIndenterMT"); //Constants: private static final String LINE_SEP = System.getProperty("line.separator", "\r\n"); /** * The default value to replace each tab with--equal to three spaces. * @see #getReplacementSpaces() */ public static final String SPACES_TO_RPLC_WITH_DEFAULT = " "; /** * <p>Create a new instance with the default spaces-replacement.</p> * * <p>Equal to * <br> <code>{@link #TabToSpaceIndenterMT(String) this}(SPACES_TO_RPLC_WITH_DEFAULT)</code></p> */ public TabToSpaceIndenterMT() { this(SPACES_TO_RPLC_WITH_DEFAULT); } /** * Create a new instance. * @param spaces_toRplcWith The spaces to replace each tab with. May * not be {@code null} or empty. Get with * {@link #getReplacementSpaces() getReplacementSpaces}{@code ()}. * @see #TabToSpaceIndenterMT() */ public TabToSpaceIndenterMT(String spaces_toRplcWith) { try { if(spaces_toRplcWith.length() == 0) { throw new IllegalArgumentException("spaces_toRplcWith has no characters."); } } catch(NullPointerException x) { Objects.requireNonNull(spaces_toRplcWith, "spaces_toRplcWith"); throw x; } rplcmntSpaces = spaces_toRplcWith; } /** * The value to replace each tab with. * @see #TabToSpaceIndenterMT(String) * @see #SPACES_TO_RPLC_WITH_DEFAULT */ public String getReplacementSpaces() { return rplcmntSpaces; } /** * <p>Utility function to replace all indentation tabs for all files in * a directory--<i><b>this overwrites all files!</b></i></p> * * <p>This function prints both <code>INFO</code> and <code>DEBUG</code> * level information, via SLF4J.</p> * * @param areYouSure_overwritePrompts If greater than zero, this is the * number of times the user is presented with a prompt to confirm * overwriting. If five, for instance, the <i>first</i> five files are * not overwritten until the user confirms each (by answering with a * <code>'Y'</code> or <code>'y'</code>. Anything else aborts the * application. * @param file_itr May not be {@code null}. <i>Every</i> returned file * in this iterator is expected to be a plain-text file (so filter it as * necessary), and both readable and writable. * <i>Should</i> be non-{@code null} and non-empty. * @param source_baseDir The directory base-path in which files are read * from. This is what is replaced by <code>dest_baseDir</code>. May not * be {@code null} and must begin the path for every file returned by * <code>file_itr</code>. * @param dest_baseDir The directory base-path that output is written * to. This is what <code>source_baseDir</code> is replaced by. May not * be {@code null} or empty. * <br>is {@code true}. * @see #getReplacementSpaces() * @see org.apache.commons.io.FileUtils#iterateFiles(File, IOFileFilter, IOFileFilter) commons.io.FileUtils#iterateFiles */ public void replaceAllInDirectory(int areYouSure_overwritePrompts, Iterator<File> file_itr, String source_baseDir, String dest_baseDir, ExecutorService exec_svc) throws IOException, InterruptedException { logger.info("Source dir: " + source_baseDir); logger.info("Destination dir: " + dest_baseDir); Objects.requireNonNull(file_itr, "file_itr"); int fileCount = 0; //How many total files were analyzed (and //potentially changed)? int totalTabsRplcd = 0; //How many tabs were replaced in *all* int aysPromptsGiven = 0; //files? List<Future<Integer>> futureTabsRplcdList = new ArrayList<>(2000); while(file_itr.hasNext()) { if(Thread.currentThread().isInterrupted()) { break; } File f = file_itr.next(); fileCount++; String sourcePath = f.getAbsolutePath(); if(!sourcePath.startsWith(source_baseDir)) { throw new IllegalArgumentException("sourcePath (" + sourcePath + ") does not start with source_baseDir (" + source_baseDir + ")."); } String destPath = dest_baseDir + sourcePath.substring(source_baseDir.length()); if(++aysPromptsGiven <= areYouSure_overwritePrompts) { String promptText = "[" + aysPromptsGiven + "/" + areYouSure_overwritePrompts + "] About to overwrite" + LINE_SEP + " " + destPath + LINE_SEP + "Are you sure? 'Y' or 'y' to " + "proceed. Anything else to abort: "; Console console = System.console(); Objects.requireNonNull(console, "System.console() (This error is " + "expected when you run this in Eclipse. This works when " + "executing it directly on the console. If you set the first " + "parameter to zero, you can safely run it in Eclipse.)"); String input = console.readLine(promptText); if(!input.toLowerCase().equals("y")) { System.out.println("Aborting."); return; } } //Replace the tabs in this file, and get the number of tabs //actually replaced. Callable<Integer> rplcAllInFile = new ReplaceAllTabsInOneFile(f, destPath, getReplacementSpaces(), logger); Future<Integer> fint = null; try { fint = exec_svc.submit(rplcAllInFile); } catch(NullPointerException npx) { throw new NullPointerException("exec_svc"); } //Can't get the result here! Well, you *can*, but then you block until //it's ready, on each iteration. futureTabsRplcdList.add(fint); } try { for(Future<Integer> fint: futureTabsRplcdList) { totalTabsRplcd += fint.get(); } } catch(InterruptedException | ExecutionException x) { throw new RuntimeException("Attempting to get the result from the ReplaceAllTabsInOneFile future: " + x); } //Print summary to console. String avgTabsPerFileStr = new DecimalFormat("#.###").format(totalTabsRplcd / new Double(fileCount)); logger.info("Done. {} total tabs replaced in {} total files ({}/file)", totalTabsRplcd, fileCount, avgTabsPerFileStr, logger); if(Thread.currentThread().isInterrupted()) { throw new InterruptedException("Interrupted. INFO-level logged summary " + "is only for the " + fileCount + " files processed so far."); } } } class ReplaceAllTabsInOneFile implements Callable<Integer> { private static final String LINE_SEP = System.getProperty("line.separator", "\r\n"); private static final boolean OVERWRITE = false; private static final boolean MANUAL_FLUSH = false; private static final DecimalFormat DEC_FMT = new DecimalFormat("#.###"); private final File srcFile; private final String destPath; private final String spaces; private final Logger logger; public ReplaceAllTabsInOneFile(File source_file, String dest_path, String spcs_toRplcWith, Logger logger) { srcFile = source_file; destPath = dest_path; spaces = spcs_toRplcWith; this.logger = logger; } public Integer call() { try { logger.trace("Getting input line iterator to {}." + srcFile.getAbsolutePath()); } catch(NullPointerException x) { Objects.requireNonNull(srcFile, "source_file"); Objects.requireNonNull(logger, "logger"); throw x; } logger.trace("Getting input line iterator to {}." + srcFile.getAbsolutePath()); Iterator<String> lineItrInput = null; try { lineItrInput = FileUtils.lineIterator(srcFile); } catch(Exception x) { throw new RuntimeException( "Attempting to obtain line iterator for \"" + srcFile.getAbsolutePath() + "\"", x); } logger.debug("Creating output print writer to dest_path (\"{}\").", destPath); PrintWriter writerOut = null; try { writerOut = (new PrintWriter(new BufferedWriter( new FileWriter(destPath, OVERWRITE)), MANUAL_FLUSH)); } catch(Exception x) { throw new RuntimeException("Attempting to create a print writer to \"" + destPath + "\"", x); } int totalLines = 0; //How many lines in this file? int tabsRplcd = 0; //How many tabs total in *all* lines? try { while(lineItrInput.hasNext()) { String line = lineItrInput.next(); totalLines++; if(line.length() == 0) { //No text at all, so definitely no tabs. writerOut.write(LINE_SEP); logger.trace("{} No characters. Writing out LINE_SEP only.", totalLines); continue; } //At least some text. int charIdx = 0; //Starting at the left-most character, while it's a tab, //replace it with spaces. while(charIdx < line.length() && line.charAt(charIdx) == '\t') { //There *is* another character in the line, and it *is* a //tab. charIdx++; writerOut.write(spaces); tabsRplcd++; logger.trace("{}. {} tabs replaced.", totalLines, tabsRplcd); } //No more tabs. Append the rest of the line. writerOut.write(line.substring(charIdx)); writerOut.write(LINE_SEP); logger.trace("{}. No more tabs in this file. Appending the rest of the line.", totalLines); } } catch(Exception x) { throw new RuntimeException("srcFile=" + srcFile.getAbsolutePath() + ", destPath=" + destPath + ""); } finally { writerOut.flush(); writerOut.close(); logger.trace("{}. Writer flushed and closed.", totalLines); } String avgTabsPerLineStr = DEC_FMT.format(tabsRplcd / new Double(totalLines)); logger.debug("This file: {} lines, {} tabs, {} average per line", totalLines, tabsRplcd, avgTabsPerLineStr); return tabsRplcd; } }