闲来无事,写了一个字符串分段对比的方法:两个字符串,将字符串1分为多个段,逐段与字符串2整体进行比较,统计比较的结果,并计算字符串1所有匹配上字符串2的段占字符串1所有分段总数的比例,即此方法中的相似百分比。
该方法仅为一个初步想法,其结果可以作为一个参考,但也可能没有实际意义,只当是对多线程的一次学习与实践。另外感受一下对代码的性能优化:
- 其中方法【utils.StringUtils.compareStringThread(String, String, int, int, boolean)】相较方法【utils.StringUtils.compareStringThread_old(String, String, int, int, boolean)】做了一次优化(前者的方法注释上标明了优化方案及方案间针对时间及空间的比较),降低了最大内存占用量和方法执行时间);
- 读取txt文件内容的方法【utils.FileUtils.getContentFromTextFile(File, String, boolean)】,最开始用的是【RandomAccessFile】,但发现读大文件时实在是太慢了(读一个50MB的文件花了300多秒),网上一查,发现原因在于【RandomAccessFile】是读一个字节做一次转码,而【BufferedReader】这种是读一个缓冲区,再对缓冲区中的内容整个进行转码。
接下来贴出比较方法的代码(utils.StringUtils),附带一个文件操作的共通类(utils.FileUtils),包含文件的读取与文件路径的获取等。
package utils;
import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class StringUtils {
public static Integer compareStr = 0;// 字符串1匹配上字符串2的段数
/**
* 字符串比较(多线程处理,按多个分段进行比较)
* @deprecated 大数据量下效率较低,内存空间占用太大,以方法{@link utils.StringUtils#compareStringThread(String, String, int, int, boolean)}取代之
* @param str1 字符串1
* @param str2 字符串2
* @param paragSize 比较的分段长度(应大于等于1,若小于0则默认字符串1的长度(后期会变),其他特殊选项:0,字符串1的长度;)
* @param threadCount 线程数(若参数小于1,则默认为1个线程)
* @param isShowLog 是否展示日志
* @return 比较结果:
* str1Length:字符串1总长度;
* str2Length:字符串2总长度;
* str1SumParag:字符串1总段数;
* str2SumParag:字符串2总段数;
* strComParag:字符串1匹配上字符串2的段数;
* str1ComParagPropor:字符串1匹配上字符串2的段数占字符串1总段数的比例;
* str2ComParagPropor:字符串1匹配上字符串2的段数占字符串2总段数的比例(该项仅供参考,不具有说明性:若该比例大于100%,则说明出现字符串1中多段重复匹配字符串2的情况,此时可考虑增加段长度);
*/
public static Map<String,Double> compareStringThread_old(String str1, String str2, int paragSize, int threadCount, boolean isShowLog){
// 参数处理
// 比较的分段长度
if (paragSize <= 0) {
paragSize = str1.length();
threadCount = 1;
}
// 线程数
if (threadCount < 1) {
threadCount = 1;
}
// 执行开始时间
long startTime = 0l;
if (isShowLog) {
startTime = (new Date()).getTime();
System.out.println("------------ 开始执行比对……");
System.out.println("分段长度 = "+paragSize);
System.out.println("字符串1总长度 = "+str1.length());
System.out.println("字符串2总长度 = "+str2.length());
}
// 返回结果
Map<String,Double> result = new HashMap<String,Double>();
// 取得所有段
List<String> str1Parags = new ArrayList<String>();// 字符串1的所有段
for (int i=0; i<str1.length()-paragSize+1; i++) {
str1Parags.add(str1.substring(i, i+paragSize));
}
// 比对两个字符串,取其中某一段编码进行对比,得出总段数与匹配上的段数以及匹配上的比例
int pageSize = str1Parags.size()/threadCount;// 单个线程需要比对的集合数
int fromIndex = 0;
int toIndex = pageSize;
List<Thread> works = new ArrayList<Thread>();
if (isShowLog) {
System.out.println("单个线程比对的段数 = "+pageSize);
System.out.println("比对字符串的长度 = "+str2.length());
System.out.println("------------ 线程共【"+threadCount+"】开始比对……");
}
// 多线程处理
for (int i=1; i<=threadCount; i++) {
List<String> stringList = str1Parags.subList(fromIndex, toIndex);
Thread thread = new Thread(new Runnable(){
@Override
public void run() {
int compareTimes = 0;
for (String str1Parag : stringList) {
if (str2.indexOf(str1Parag) >= 0) {
compareTimes++;
}
}
if (isShowLog) {
System.out.println("------ 单个比对结束:本次比对总数count="+stringList.size()+";本次比对成功count="+compareTimes);
}
// 为单个线程锁定compareStr对象,以对compareStr同步访问
synchronized (compareStr) {
compareStr += compareTimes;
}
}
},"Thread"+i);
thread.start();
works.add(thread);
fromIndex += pageSize;
toIndex += pageSize;
// 最后一个线程应被分配剩余所有分段
if (i == (threadCount - 1)) {
toIndex = str1Parags.size();
}
}
// 等待线程结束
for (Thread thread : works) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 设置结果
result.put("str1Length", Double.parseDouble(String.valueOf(str1.length())));// 字符串1总长度
result.put("str2Length", Double.parseDouble(String.valueOf(str2.length())));// 字符串2总长度
result.put("str1SumParag", Double.parseDouble(String.valueOf(str1Parags.size())));// 字符串1总段数
result.put("str2SumParag", Double.parseDouble(String.valueOf(paragSize > str2.length() ? 1 : str2.length()-paragSize+1)));// 字符串2总段数
result.put("strComParag", Double.parseDouble(String.valueOf(compareStr)));// 字符串1匹配上字符串2的段数
result.put("str1ComParagPropor", result.get("strComParag")/result.get("str1SumParag"));// 字符串1匹配上字符串2的段数占字符串1总段数的比例
result.put("str2ComParagPropor", result.get("strComParag")/result.get("str2SumParag"));// 字符串1匹配上字符串2的段数占字符串2总段数的比例
if (isShowLog) {
System.out.println("字符串1匹配上字符串2的段数 = "+result.get("strComParag"));
System.out.println("字符串1总段数 = "+result.get("str1SumParag"));
System.out.println("字符串2总段数 = "+result.get("str2SumParag"));
System.out.println("字符串1匹配上字符串2的段数占字符串1总段数的比例 = "+String.format("%.6f", result.get("str1ComParagPropor")*100) + "%");
System.out.println("字符串1匹配上字符串2的段数占字符串2总段数的比例(仅供参考) = "+String.format("%.6f", result.get("str2ComParagPropor")*100) + "%");
// 特殊关系
String relationship = "无";
if (result.get("str1ComParagPropor") == 1 && result.get("str2ComParagPropor") == 1) {
relationship = "完全相同";
} else if (result.get("str2ComParagPropor") > 1) {
relationship = "前者中多段重复匹配后者";
} else if (result.get("str1ComParagPropor") == 1 && str1.length() == paragSize) {
relationship = "前者是后者中的一段";
} else if (result.get("str1ComParagPropor") == 1 && str1.length() > paragSize) {
relationship = "前者(可能)是后者中的一段";
}
System.out.println("字符串1与字符串2的特殊关系:【"+relationship+"】");
// 执行结束时间
long endTime = (new Date()).getTime();
System.out.println("------------ 方法执行消耗的时间:"+ ((endTime-startTime) > 1000 ? ((endTime-startTime)/1000 + "s") : ((endTime-startTime) + "ms ------------")));
}
return result;
}
/**
* 字符串比较(多线程处理,按多个分段进行比较)
* <p>大数据量下方案(此方法为方案2实现):</p>
* 1、先分隔字符串,在每一个线程处理对应字符串;
* <li>空间:①原字符串;②分割后的字符串(大小大于原字符串,原字符串长时相当);</br>
* ①分割后的字符串(大小大于原字符串,原字符串长时相当);②每个线程分配的段;</li>
* <li>时间相比方案2多出的:①几十次字符串截取;②针对原字符串的析构时间;</li>
* <li>空间相比方案2多出的:①分隔后的字符串的重叠部分;</li>
* 2、不分隔字符串,直接在线程中取对应的段(多线程同时读取某一资源时不存在死锁问题);
* <li>空间:①原字符串;②每个线程分配的段;</li></br>
* @param str1 字符串1
* @param str2 字符串2
* @param paragSize 比较的分段长度(应大于等于1,若小于0则默认字符串1的长度(后期会变),其他特殊选项:0,字符串1的长度;)
* @param threadCount 线程数(若参数小于1,则默认为1个线程)
* @param isShowLog 是否展示日志
* @return 比较结果:
* str1Length:字符串1总长度;
* str2Length:字符串2总长度;
* str1SumParag:字符串1总段数;
* str2SumParag:字符串2总段数;
* strComParag:字符串1匹配上字符串2的段数;
* str1ComParagPropor:字符串1匹配上字符串2的段数占字符串1总段数的比例;
* str2ComParagPropor:字符串1匹配上字符串2的段数占字符串2总段数的比例(该项仅供参考,不具有说明性:若该比例大于100%,则说明出现字符串1中多段重复匹配字符串2的情况,此时可考虑增加段长度);
*/
public static Map<String,Double> compareStringThread(String str1, String str2, int paragSize, int threadCount, boolean isShowLog){
// 参数处理
// 比较的分段长度
if (paragSize <= 0) {
paragSize = str1.length();
threadCount = 1;
}
// 线程数
if (threadCount < 1) {
threadCount = 1;
}
// 执行开始时间
long startTime = 0l;
if (isShowLog) {
startTime = (new Date()).getTime();
System.out.println("------------ 开始执行比对……");
System.out.println("分段长度 = "+paragSize);
System.out.println("字符串1总长度 = "+str1.length());
System.out.println("字符串2总长度 = "+str2.length());
}
// 返回结果
Map<String,Double> result = new HashMap<String,Double>();
// 比对两个字符串,取其中某一段编码进行对比,得出总段数与匹配上的段数以及匹配上的比例
int paragsCount;// 字符串1的总分段数
if (paragSize > str1.length()) {
paragsCount = 1;
threadCount = 1;
} else {
paragsCount = str1.length() - paragSize + 1;
}
int pageSize = (int)Math.ceil((double)paragsCount/(double)threadCount);// 每个线程应比对段数(上取整,最后一个线程所分配段数<=pageSize,开的总线程数<=threadCount)
List<Thread> works = new ArrayList<Thread>();
if (isShowLog) {
System.out.println("单个线程比对的段数 = "+pageSize);
System.out.println("比对字符串的长度 = "+str2.length());
System.out.println("------------ 线程共【"+threadCount+"】开始比对……");
}
// 多线程处理
for (int i=1; i<=threadCount; i++) {
// 解决java内部类访问局部变量时局部变量必须声明为final的问题
final int fromIndex = (i - 1) * pageSize;
// 开的总线程数<=threadCount
if (fromIndex > paragsCount) {
break;
}
final int paragSizeParam = paragSize;
Thread thread = new Thread(new Runnable(){
// 分配给该线程的字符串1的段
private List<String> stringList = new ArrayList<String>();
@Override
public void run() {
// 分配给该线程对应段
for (int j=fromIndex; j<fromIndex+pageSize; j++) {
// 最后一个线程可能不足该pageSize个段
if (j+paragSizeParam > str1.length()) {
break;
}
this.stringList.add(str1.substring(j, j+paragSizeParam));
}
int compareTimes = 0;
for (String str1Parag : this.stringList) {
if (str2.indexOf(str1Parag) >= 0) {
compareTimes++;
}
}
if (isShowLog) {
System.out.println("------ 单个比对结束:本次比对总数count="+stringList.size()+";本次比对成功count="+compareTimes);
}
// 为单个线程锁定compareStr对象,以对compareStr同步访问
synchronized (compareStr) {
compareStr += compareTimes;
}
}
},"Thread"+i);
thread.start();
works.add(thread);
}
// 等待线程结束
for (Thread thread : works) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 设置结果
result.put("str1Length", Double.parseDouble(String.valueOf(str1.length())));// 字符串1总长度
result.put("str2Length", Double.parseDouble(String.valueOf(str2.length())));// 字符串2总长度
result.put("str1SumParag", Double.parseDouble(String.valueOf(paragsCount)));// 字符串1总段数
result.put("str2SumParag", Double.parseDouble(String.valueOf(paragSize > str2.length() ? 1 : str2.length()-paragSize+1)));// 字符串2总段数
result.put("strComParag", Double.parseDouble(String.valueOf(compareStr)));// 字符串1匹配上字符串2的段数
result.put("str1ComParagPropor", result.get("strComParag")/result.get("str1SumParag"));// 字符串1匹配上字符串2的段数占字符串1总段数的比例
result.put("str2ComParagPropor", result.get("strComParag")/result.get("str2SumParag"));// 字符串1匹配上字符串2的段数占字符串2总段数的比例
if (isShowLog) {
System.out.println("字符串1匹配上字符串2的段数 = "+result.get("strComParag"));
System.out.println("字符串1总段数 = "+result.get("str1SumParag"));
System.out.println("字符串2总段数 = "+result.get("str2SumParag"));
System.out.println("字符串1匹配上字符串2的段数占字符串1总段数的比例 = "+String.format("%.6f", result.get("str1ComParagPropor")*100) + "%");
System.out.println("字符串1匹配上字符串2的段数占字符串2总段数的比例(仅供参考) = "+String.format("%.6f", result.get("str2ComParagPropor")*100) + "%");
// 特殊关系
String relationship = "无";
if (result.get("str1ComParagPropor") == 1 && result.get("str2ComParagPropor") == 1) {
relationship = "完全相同";
} else if (result.get("str2ComParagPropor") > 1) {
relationship = "前者中多段重复匹配后者";
} else if (result.get("str1ComParagPropor") == 1 && str1.length() == paragSize) {
relationship = "前者是后者中的一段";
} else if (result.get("str1ComParagPropor") == 1 && str1.length() > paragSize) {
relationship = "前者(可能)是后者中的一段";
}
System.out.println("字符串1与字符串2的特殊关系:【"+relationship+"】");
// 执行结束时间
long endTime = (new Date()).getTime();
System.out.println("------------ 方法执行消耗的时间:"+ ((endTime-startTime) > 1000 ? ((endTime-startTime)/1000 + "s") : ((endTime-startTime) + "ms ------------")));
}
return result;
}
/**
* 字符串比较(多线程处理,按多个分段进行比较,不输出日志)
* @param str1 字符串1
* @param str2 字符串2
* @param paragSize 比较的分段长度
* @param threadCount 线程数
* @return 比较结果,参见方法{@link utils.StringUtils#compareStringThread(String, String, int, int, boolean)}
*/
public static Map<String,Double> compareStringThread(String str1, String str2, int paragSize, int threadCount){
return compareStringThread(str1, str2, paragSize, threadCount, false);
}
/**
* 字符串比较(多线程处理,按多个分段进行比较,不输出日志, 默认20个线程)
* @param str1 字符串1
* @param str2 字符串2
* @param paragSize 比较的分段长度
* @return 比较结果,参见方法{@link utils.StringUtils#compareStringThread(String, String, int, int, boolean)}
*/
public static Map<String,Double> compareStringThread(String str1, String str2, int paragSize){
return compareStringThread(str1, str2, paragSize, 20, false);
}
/**
* 对两个文本文件中的内容(字符串)比较(多线程处理,按多个分段进行比较)
* @param textFilePath1 文本文件1路径
* @param textFilePath2 文本文件2路径
* @param paragSize 比较的分段长度(应大于等于1,其他特殊选项:0,文本文件1的长度;)
* @param threadCount 线程数(若参数为0,则默认为1个线程)
* @param isShowLog 是否展示日志
* @return 比较结果:
* file1Length:文本文件1总长度;
* file2Length:文本文件2总长度;
* file1SumParag:文本文件1总段数;
* file2SumParag:文本文件2总段数;
* fileComParag:文本文件1匹配上文本文件2的段数;
* file1ComParagPropor:文本文件1匹配上文本文件2的段数占文本文件1总段数的比例;
* file2ComParagPropor:文本文件1匹配上文本文件2的段数占文本文件2总段数的比例(该项仅供参考,不具有说明性:若该比例大于100%,则说明出现字符串1中多段重复匹配字符串2的情况,此时可考虑增加段长度);
*/
public static Map<String,Double> compareTextFileThread(String textFilePath1, String textFilePath2, int paragSize, int threadCount, boolean isShowLog){
String charsetName = "GBK";
if (textFilePath1 == null || textFilePath2 == null) {
if (isShowLog) System.out.println("请确保文件存在!");
return null;
}
String str1 = FileUtils.getContentFromTextFile(new File(textFilePath1), charsetName, isShowLog);
String str2 = FileUtils.getContentFromTextFile(new File(textFilePath2), charsetName, isShowLog);
return compareStringThread(str1, str2, paragSize, threadCount, isShowLog);
}
public static void main(String[] args) {
String str1 = FileUtils.getFilePath("test/textFiles/file4.txt");
String str2 = FileUtils.getFilePath("test/textFiles/file4.txt");
compareTextFileThread(str1,str2,5,20,true);
}
}
文件操作的共通类:
package utils;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.util.Date;
public class FileUtils {
public static final String PROJECT_NAME = System.getProperty("user.dir").substring(System.getProperty("user.dir").lastIndexOf(File.separator)+1, System.getProperty("user.dir").length());
/**
* 根据uri取得文件路径
* @param uri
* @return 文件绝对路径(异常则返回null)
*/
public static String getPathFromURI(URI uri) {
try {
File file = new File(new URI(uri.toString()));
return file.getAbsolutePath();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据uri取得文件路径
* @param uriStr
* @return 文件绝对路径(异常则返回null)
*/
public static String getPathFromURI(String uriStr) {
try {
File file = new File(new URI(uriStr));
return file.getAbsolutePath();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据url取得文件路径
* @param url
* @return 文件绝对路径(异常则返回null)
*/
public static String getPathFromURL(URL url) {
try {
File file = new File(url.toURI());
return file.getAbsolutePath();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据url取得文件路径
* @param urlStr
* @return 文件绝对路径(异常则返回null)
*/
public static String getPathFromURL(String urlStr) {
try {
File file = new File((new URL(urlStr)).toURI());
return file.getAbsolutePath();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据相对路径取得文件
* @param relativePath 相对项目目录的路径
* @return 对应文件(文件不存在则返回null)
*/
public static File getFile(String relativePath){
if (relativePath == null) {
return null;
}
if (".".equals(relativePath)) {
relativePath = "";
}
// 相对路径开头没有路径分隔符
if (relativePath.indexOf("/") != 0 && relativePath.indexOf("\\") != 0) {
relativePath = File.separator + relativePath;
}
File file = new File(System.getProperty("user.dir") + relativePath);
return file.exists() ? file : null;
}
/**
* 根据相对路径取得文件绝对路径
* @param relativePath 相对项目目录的路径
* @return 对应文件绝对路径
*/
public static String getFilePath(String relativePath){
File file = getFile(relativePath);
return file != null ? file.getAbsolutePath() : null;
}
/**
* 读取文本文件内容(默认不展示日志,默认UTF-8)
* @param file 文本文件
* @return 文本文件内容
*/
public static String getContentFromTextFile(File file){
return getContentFromTextFile(file,"UTF-8",false);
}
/**
* 读取文本文件内容(默认不展示日志)
* @param file 文本文件
* @param charsetName 字符编码
* @return 文本文件内容
*/
public static String getContentFromTextFile(File file, String charsetName){
return getContentFromTextFile(file,charsetName,false);
}
/**
* 读取文本文件内容
* @param file 文本文件
* @param isShowLog 是否展示日志
* @param charsetName 字符编码
* @return 文本文件内容
*/
public static String getContentFromTextFile(File file, String charsetName, boolean isShowLog){
long startTime = 0l;
if (isShowLog) {
startTime = (new Date()).getTime();
System.out.println("------------ 读取文件开始:文件【"+file.getPath()+"】 ------------");
}
StringBuffer result = new StringBuffer();
BufferedReader reader = null;
InputStreamReader isr = null;
try {
isr = new InputStreamReader(new FileInputStream(file), charsetName);
reader = new BufferedReader(isr);
String tempString = null;
// 一次读一行,读入null时文件结束
tempString = reader.readLine();
while (tempString != null) {
result.append(tempString);
tempString = reader.readLine();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if (isr != null) {
try {
isr.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
if (isShowLog) {
long endTime = (new Date()).getTime();
System.out.println("------------ 读取文件结束,共耗时:"+((endTime-startTime) > 1000 ? ((endTime-startTime)/1000 + "s") : ((endTime-startTime) + "ms")) + " ------------");
}
return result.toString();
}
public static void main(String[] args) {
String str = getContentFromTextFile(getFile("test\\textFiles\\file5.txt"),"GBK",true);
System.out.println(str.length());
System.out.println(str);
}
}