前言

近日,笔者在接到一项需求,就是用程序将两个文本的内容以行为单位进行比对,找出其中差异的部分进行展示,以便能够一眼看出修改人对文件做出了哪些修改。
刚接到这项需求时,感到颇有难度,但是经过深入思考,终于想出来实现文本内容对比的算法,并且写成程序得以实现。现将算法和代码公布,欢迎各位软件研发人员、热爱算法的同仁阅读和交流。
笔者QQ:1072334275,如果任何问题,请加笔者QQ。

人的思维是怎么比对文本的?

刚开始,我对这个问题也一筹莫展,因为一行行的去进行两个文本对比的话,如果新文本中有增加或者删除的行,那么行与行的对应关系就全乱了。后来我开始想,我们人的思维在进行文本内容对比的时候是怎样的一个过程呢?既然我们人类大脑的思维具有足够的对比文本内容差异的智能,那么人脑的思维过程就完全可以借鉴一下。经过思考,我总结出人脑的思维在进行文本内容差异比对的时候的过程。

为了描述形象,采用以下新旧两个文本示例来描述。

1、首先我们的目光会将将新旧文本分别从第一行开始,逐行去看两行的内容是否一样。

2、当我们第一次看到两行的内容不一样时,我们的大脑就会想,已经开始进入内容有区别的区域了(下文称差异区域)。对于示例图的两个文本,新旧文本的差异区域的起点均是第5行,即从第5行开始,两个文本出现差异。

3、随后我们会在两个文本里面继续往下看,直到再次看到两个内容相同的行为止,这时我们的大脑就会认为差异区域已经结束。
但是,我们千万不要把问题想得这么简单。因为如果修改内容是删除或者增加了若干行,那么新旧文本之间的行与行的对应关系就会乱掉。为此我们的大脑做了一个我们可能不容易发现的动作。那就是从差异区域的起点开始,如果发现下文找不到差异区域的终点,那么试探性的将新文本或旧文本进行整体性的上移若干行,然后再逐行对比移动后的文本。例如如果是新文本中新增了N行,则需要将新文本的所有内容整体性上移N行数;如果是旧文本中删除了N行,也需要将旧文本整体性的上移N行数,这样才能对比出差异区域的终点位置。

对于示例图的两个文本,需要将新文本上移两行才能找到差异区域的终点。旧文本差异区域的终点是第5行,新文本差异区域的终点是第7行。
具体过程如图所示:

4、当我们的大脑分别在新旧文本里面捕捉到差异区域的开始点与结束点以后,我们的大脑的就会让我们不要再继续往下看了,要先分析一下差异区域里面是被做了怎样的修改。

5、我们的大脑会依据新旧文本在差异区域上各自的起点和终点,来判断文本的变化类型。
为了方便描述,我们不妨将旧文本差异区域的起点行号为oldStart,旧文本差异区域的终点行号为oldEnd;将新文本差异区域的起点行号为newStart,新文本差异区域的终点行号为newEnd。
这里先直接给出一个结论,具体推导过程见下文:如果oldEnd!=oldStart && newEnd!=newStart && (oldEnd-oldStart)<(newEnd-newStart),说明变化类型是既有新增也有修改了若干行。示例图中的oldStart、oldEnd、newStart、newEnd分别为5、6、5、8,说明其属于新增且修改了若干行。

差异区域的变化类型有5种,分别是纯粹新增若干行、纯粹删除若干行、纯粹修改若干行、新增并修改若干行、删除并修改若干行。

当然还有删除并新增若干行这种情况,但是删除之后再新增,变化的结果等同于修改,所以此种类型可以直接合并到以上5种变化类型之内。

6、如果判断出差异区域的变化类型中有修改,我们的思维就会开始去比较那些修改前后的行的内容,观察它们重复字符的数量,将重复字符最多的两行认定为修改前后的两行。
例如示例图中的“回眸三笑百媚生”和“回眸一笑百媚生”两行的重复字符最多,说明这两行属于修改前后的两行。

7、当我们的大脑在已经得出第一处差异区域的变化类型和内容后,就会命令我们的眼睛从本次差异区域的终点开始(这点很重要,必须是从差异区域的终点开始),接着对比剩余的文本。

算法与代码

第一步:读取新旧文本内容

读取文本内容的每一行,直接将每行存入一个ArrayList集合,ArrayList元素的下标可以作为行号来使用。

//此方法用于读取文件,并且去除空行static public List<String> readFile(String path) {BufferedReader reader = null;File file = new File(path);if(!file.exists()) {System.out.println("文件不存在");}String tempStr;try {reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "utf-8"));List<String> lines=new ArrayList<>();while ((tempStr = reader.readLine()) != null) {//读取文本时,每一行采用行号+行文本内容键值对的形式进行存储,行号作为该行的唯一标识if(!tempStr.trim().equals("")){lines.add(tempStr);}}return lines;} catch (IOException e) {e.printStackTrace();return null;}finally {if(reader!=null) {try {reader.close();} catch (IOException e) {e.printStackTrace();}}}}

第二步:寻找差异区域的起点和终点

差异区域的起点很好找,直接逐行比对即可。
但是差异区域的终点就要麻烦很多,因为需要考虑三种情况:
1、新文本增加了若干行,导致行与行之间的对应关系错乱;
2、旧文本删除了若干行,导致行与行之间的对应关系错乱;
3、先删除了N行,再在下文新增了相同数量N行。导致逐行比对时将删除与新增中间的区域也误认为是差异区域。

对于第1种情况,可以将新文本上移。如下面的文本,新文本新增了“xxx”,直接上移一行,再逐行比对就可以得到差异区域的终点为旧文本的第2行与新文本的第3行。

对于第2种情况,可以将旧文本上移。如下面的文本,旧文本删除了“bbb”,将旧文本上移1行,就可以得到差异区域的终点为旧文本的第3行与新文本的第1行。

对于第3种情况,最难处理。例如下面的变化类型是旧文本中删除第2行,新文本中增加第5行。但是如果逐行比对,也能找到差异区域的终点为旧文本的第6行与新文本的第6行。于是很容易将2、3、4、5行都作为差异区域,变化类型全部为修改。

因此,在逐行比对得到终点以后,仍然需要试探性的将新文本或旧文本上移,观察移动文本之后能否得到离起点更近的终点,如果有,则取最近的。
例如将旧文本上移1行就可以得到真实的差异区域终点为旧文本的第3行和新文本的第2行。

相关代码为:

       /*** 逐行对比文本,找到第一个不同或者相同的行* 其中的oldLines和newLines为成员属性,表示旧文本的每一行和新文本的每一行* type:是找第一个内容相同的行还是不同的行,true为找相同,false为找不同* oldLineStart:旧文本从第几行开始* newLineStart:新文本从第几行开始* 返回true表示寻找成功,返回false表示寻找失败*/static public boolean compareLines(boolean type,int oldLineStart,int newLineStart){if(oldLineStart >= oldLines.size() || newLineStart >= newLines.size()){return  false;}//行号计数器int lineCount = 0;//开始逐行比对两个文本int oldLineNumber,newLineNubmer;while ((oldLineNumber=oldLineStart+lineCount)<oldLines.size() && (newLineNubmer=newLineStart+lineCount)<newLines.size()){//分别取出新旧文本中的一行String lineOld = oldLines.get(oldLineNumber);String lineNew = newLines.get(newLineNubmer);//下面代码中的oldEnd、oldStart、newEnd、newStart为实例属性,//分别表示旧文本差异区域的起终点和新文本差异区域的起终点//找到完全相同的两行,其可以作为差异区域的终点if(type && lineOld.equals(lineNew)){//如果是第一次找到终点,先记录在oldEnd、newEnd两个属性中if(isFirstGetEnd){oldEnd = oldLineNumber;newEnd = newLineNubmer;isFirstGetEnd = false;//如果不是第一次找到,比较哪个终点与起点最近,取最近的终点}else if(newLineNubmer<newEnd){oldEnd = oldLineNumber;newEnd = newLineNubmer;}return true;}//找到差异的两行,其可以作为差异区域的起点if(!type && !lineOld.equals(lineNew)){oldStart = oldLineNumber;newStart = newLineNubmer;return true;}lineCount++;}//到文本的最后还没找到,返回falsereturn false;}//在新旧文本寻找差异区域的起点,oldLines和newLines分别为存储新旧文本行内容的Map集合static public boolean getDifferenceAreaStart() {return compareLines(false,oldEnd,newEnd);}//寻找差异区域的终点,也就是新旧文本重新复合的点。static public boolean getDifferenceAreaEnd() {//重置为trueisFirstGetEnd = true;//标记是否找到终点boolean haveEnd = false;//moveLines为文本下移的行数int moveLines = 0;int oldLineNumber=oldStart,newLineNubmer=newStart;while ((oldLineNumber<oldLines.size() || newLineNubmer < newLines.size())){//newStart为0时不移动文本,newStart大于0时尝试以移动文本的方式来找终点if(compareLines(true,oldLineNumber,newStart) || compareLines(true,oldStart,newLineNubmer)){haveEnd = true;}moveLines ++;oldLineNumber = oldStart + moveLines;newLineNubmer = newStart + moveLines;}return haveEnd;}

第三步:判断变化类型

现在我们已经得到差异区域的起点和终点了,不知道读者还是否记得上文所说的,这时我们的大脑不会再让我们往下阅读,而是转而判断差异区域的变更类型。

关于如何判断差异区域的变化类型,需要使用新旧文本的差异区域的起点行号与终点行号来进行分析,这里我们不妨先定义一下,以方便书写:

旧文本差异区域的起点行号为oldStart,旧文本差异区域的终点行号为oldEnd;
新文本差异区域的起点行号为newStart,新文本差异区域的终点行号为newEnd;

然后,我来为你演示每种变化类型:

(1)、如果oldStart=oldEnd&&newStart<newEnd,说明变化类型是纯粹的新增了若干行:
如下面所示,我们在文本中新增了一行,内容为“ddd”。那么旧文本中差异区域的起点和终点均为第2行,即内容为“bbb”行,新文本中差异区域的起点和终点则分别为第2行和第3行。


(2)、如果oldEnd<oldStart&&newEnd<newStart&&(oldEnd-oldStart)==(newEnd-newStart),则说明变化类型是纯粹的修改了若干行:
如下图所示:我们将文本中第二行的内容由“bbb”修改为“ddd”,则旧文本中差异区域的起点和终点分别为2和3,新文本中差异区域的起点和终点则同样分别为第2行和第3行。

(3)如果(oldEnd-oldStart)>(newEnd-newStart)&&newEnd==newStart,说明文本的变化类型是纯粹的被删除了若干行。

(4)如果oldEnd!=oldStart&&newEnd!=newStart&&(oldEnd-oldStart)<(newEnd-newStart),说明变化类型是既有新增也有修改了若干行。
如下图所示,我们将文本中第二行的内容由“bbb”修改为“bbx”,再新增“ddd”。则旧文本中差异区域的起点和终点分别为2和3,新文本中差异区域的起点和终点则分别为第2行和第4行。

(5)如果oldEnd!=oldStart&&newEnd!=newStart&&(oldEnd-oldStart)>(newEnd-newStart),说明变化类型是既有删除也有修改了若干行。
如下图所示,我们将文本中第二行的内容由“bbb”修改为“bbx”,再删除“ccc”。则旧文本中差异区域的起点和终点分别为2和4,新文本中差异区域的起点和终点则分别为第2行和第3行。

代码:

    //分析文本的变化类型,存入结果集合中public static void analChangeType() {//下面开始分析差异区域的变化类型,然后按照类型进行处理//oldEnd、oldStart、newEnd、newStart为实例属性,//分别表示旧文本差异区域的起终点和新文本差异区域的起终点int oldNumDiff = oldEnd-oldStart;int newNumDiff = newEnd-newStart;//纯修改if(oldNumDiff == newNumDiff){int number=oldEnd-oldStart;for(int i = 0 ;i<number ;i++){updateLines.put(oldStart+i,newStart+i);}}else if(oldNumDiff > newNumDiff){if(newEnd==newStart){//纯删除for(int i=oldStart;i<oldEnd;i++) {//取出被删除的行,存入集合delLines.add(i);}}else {//删除加修改//计算修改的行数int updateNum=newNumDiff;//获取修改的行,getUpdateLines为获取修改行对于关系的方法,// 返回的Map中存储的是修改前后的行号Map<Integer, Integer> changeLineMap=getUpdateLines(updateNum);updateLines.putAll(changeLineMap);//获取删除的行for(int lineNum = oldStart ;lineNum <oldEnd ; lineNum ++){if(!changeLineMap.containsKey(lineNum)){delLines.add(lineNum);}}}}else {if(oldEnd==oldStart){//纯新增for(int i=newStart;i<newEnd;i++) {addLines.add(i);}}else {//新增加修改//此时修改的行数是:int number=oldNumDiff;//获取修改的旧文本行号与新文本行号组成键值对的集合Map<Integer, Integer> changeLineMap = getUpdateLines(number);updateLines.putAll(changeLineMap);//获取新增的行for(int lineNum = newStart ;lineNum <newEnd ; lineNum ++){if(!changeLineMap.values().contains(lineNum)){addLines.add(lineNum);}}}}}

第四步:寻找修改前后的两行对应关系

在确定了差异区域的变化类型后,我们要使得程序具有高度智能的特性,就需要再进行一步计算。即如果变化类型是新增且修改了若干行或者删除且修改,我们要使用程序分析出修改前的行与修改后的行之间的关联关系,即那些行是修改了,哪些行是新增或删除的。
如果寻找修改前后的两行关联,笔者采用的方法是将新旧文本差异区域内的行均取出,两两组合,计算每对组合中,两行的重复字符数量,以重复字符最多的组合作为修改前后的两行。

以下为程序代码

    //准备方法,计算两个字符串相同字符的数量static public int numJewelsInStones(String J, String S) {J=J.trim();S=S.trim();char[] Ja = J.toCharArray();char[] Sa = S.toCharArray();int r = 0;for (int i = 0;i < Ja.length ; i ++){for(int j = 0; j < Sa.length; j++){if(Ja[i] == Sa[j])r ++;}}return r;}//找出差异区域内哪些是修改的行//参数n表示我们需要找的修改前后的行有几对public static Map<Integer, Integer> getUpdateLines(int n) {Map<Integer, Integer> resultMap=new HashMap();//准备集合,用来储存组队两行的重复字符个数与各自的行号SortedMap<Integer,String> samCharMap = new TreeMap<Integer, String>(new Comparator<Integer>() {@Overridepublic int compare(Integer number1, Integer number2) {//从大到小排序return number2 - number1;}});for(int i = oldStart; i < oldEnd ; i++){for(int j = newStart ; j <newEnd ;j++){int count=numJewelsInStones(oldLines.get(i),newLines.get(j));samCharMap.put(count,String.valueOf(i)+":"+String.valueOf(j));}}int i=0;String lineInfo;Iterator<Integer> it = samCharMap.keySet().iterator();while (i<n && it.hasNext()){lineInfo = samCharMap.get(it.next());String[] lineNumA=lineInfo.split(":");resultMap.put(Integer.valueOf(lineNumA[0]),Integer.valueOf(lineNumA[1]));i++;}return resultMap;}

第五步:继续递归分析剩余文本

此时一次差异区域的分析已经完成,继续往下逐行比对文本内容。

//递归比对文本
public static void compare(){//如果能找到差异区域的起点if(getDifferenceAreaStart()){//也能找到差异区域的终点if(getDifferenceAreaEnd()){analChangeType();compare();}else {//如果找不到差异区域的终点,说明从起点开始下文全是差异区域oldEnd = oldLines.size();newEnd = newLines.size();analChangeType();}}}

完整代码

package practice;
import java.io.*;
import java.util.*;public class ContrastFile {//旧文本中差异区域的起点static int oldStart = 0;//旧文本中差异区域的终点static int oldEnd = 0;//新文本中差异区域的起点static int newStart = 0;//新文本中差异区域的终点static int newEnd = 0;//在寻找一块差异区域的终点时,是否是第一次找到终点static boolean isFirstGetEnd = true;//存储旧文本的每一行static List<String> oldLines;//存储新文本的每一行static List<String> newLines;//下面的集合用来存储对比结果//存储增加的行在新文本中的行号static List<Integer> delLines = new LinkedList<Integer>();//存储删除的行在旧文本中的行号static List<Integer> addLines = new LinkedList<Integer>();//存储修改的行分别在新旧文本中的行号static Map<Integer,Integer> updateLines = new HashMap<Integer,Integer>();public static void main(String[] args) {//作为旧文本String path1="F://comparetest/1.txt";//作为新文本String path2="F://comparetest/2.txt";//获取比对结果List<String> differMsgList = getContrastResult(path1,path2);//打印出对比结果for (String differ : differMsgList){System.out.println(differ);}}//比对文本,并收集整理对比结果public static List<String> getContrastResult(String path1,String path2){//读取文件oldLines = readFile(path1);newLines = readFile(path2);//调用对比方法compare();List<String> differMsgList = new ArrayList<String>();for(int lineNum : delLines){differMsgList.add("旧文本中删除了第"+(lineNum+1)+"行,内容是:"+oldLines.get(lineNum));}for(int lineNum : addLines){differMsgList.add("新文本中增加了第"+(lineNum+1)+"行,内容是:"+newLines.get(lineNum));}for(int oldNum : updateLines.keySet()){differMsgList.add("旧文本中的第"+(oldNum+1)+"行,内容是:"+oldLines.get(oldNum)+",修改为新文本中的第"+(updateLines.get(oldNum)+1)+"行,内容是:"+newLines.get(updateLines.get(oldNum)));}return differMsgList;}//此方法用于读取文件,并且去除空行static public List<String> readFile(String path) {BufferedReader reader = null;File file = new File(path);if(!file.exists()) {System.out.println("文件不存在");}String tempStr;try {reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "utf-8"));List<String> lines=new ArrayList<>();while ((tempStr = reader.readLine()) != null) {//读取文本时,每一行采用行号+行文本内容键值对的形式进行存储,行号作为该行的唯一标识if(!tempStr.trim().equals("")){lines.add(tempStr);}}return lines;} catch (IOException e) {e.printStackTrace();return null;}finally {if(reader!=null) {try {reader.close();} catch (IOException e) {e.printStackTrace();}}}}//准备方法,计算两个字符串相同字符的数量static public int numJewelsInStones(String J, String S) {J=J.trim();S=S.trim();char[] Ja = J.toCharArray();char[] Sa = S.toCharArray();int r = 0;for (int i = 0;i < Ja.length ; i ++){for(int j = 0; j < Sa.length; j++){if(Ja[i] == Sa[j])r ++;}}return r;}/*** 逐行对比文本,找到第一个不同或者相同的行* 其中的oldLines和newLines为成员属性,表示旧文本的每一行和新文本的每一行* type:是找第一个内容相同的行还是不同的行,true为找相同,false为找不同* oldLineStart:旧文本从第几行开始* newLineStart:新文本从第几行开始* 返回true表示寻找成功,返回false表示寻找失败*/static public boolean compareLines(boolean type,int oldLineStart,int newLineStart){if(oldLineStart >= oldLines.size() || newLineStart >= newLines.size()){return  false;}//行号计数器int lineCount = 0;//开始逐行比对两个文本int oldLineNumber,newLineNubmer;while ((oldLineNumber=oldLineStart+lineCount)<oldLines.size() && (newLineNubmer=newLineStart+lineCount)<newLines.size()){//分别取出新旧文本中的一行String lineOld = oldLines.get(oldLineNumber);String lineNew = newLines.get(newLineNubmer);//下面代码中的oldEnd、oldStart、newEnd、newStart为实例属性,//分别表示旧文本差异区域的起终点和新文本差异区域的起终点//找到完全相同的两行,其可以作为差异区域的终点if(type && lineOld.equals(lineNew)){//如果是第一次找到终点,先记录在oldEnd、newEnd两个属性中if(isFirstGetEnd){oldEnd = oldLineNumber;newEnd = newLineNubmer;isFirstGetEnd = false;//如果不是第一次找到,比较哪个终点与起点最近,取最近的终点}else if(newLineNubmer<newEnd){oldEnd = oldLineNumber;newEnd = newLineNubmer;}return true;}//找到差异的两行,其可以作为差异区域的起点if(!type && !lineOld.equals(lineNew)){oldStart = oldLineNumber;newStart = newLineNubmer;return true;}lineCount++;}//到文本的最后还没找到,返回falsereturn false;}//在新旧文本寻找差异区域的起点,oldLines和newLines分别为存储新旧文本行内容的Map集合static public boolean getDifferenceAreaStart() {return compareLines(false,oldEnd,newEnd);}//寻找差异区域的终点,也就是新旧文本重新复合的点。static public boolean getDifferenceAreaEnd() {//重置为trueisFirstGetEnd = true;//标记是否找到终点boolean haveEnd = false;//moveLines为文本下移的行数int moveLines = 0;int oldLineNumber=oldStart,newLineNubmer=newStart;while ((oldLineNumber<oldLines.size() || newLineNubmer < newLines.size())){//newStart为0时不移动文本,newStart大于0时尝试以移动文本的方式来找终点if(compareLines(true,oldLineNumber,newStart) || compareLines(true,oldStart,newLineNubmer)){haveEnd = true;}moveLines ++;oldLineNumber = oldStart + moveLines;newLineNubmer = newStart + moveLines;}return haveEnd;}//找出差异区域内哪些是修改的行//参数n表示我们需要找的修改前后的行有几对public static Map<Integer, Integer> getUpdateLines(int n) {Map<Integer, Integer> resultMap=new HashMap();//准备集合,用来储存组队两行的重复字符个数与各自的行号SortedMap<Integer,String> samCharMap = new TreeMap<Integer, String>(new Comparator<Integer>() {@Overridepublic int compare(Integer number1, Integer number2) {//从大到小排序return number2 - number1;}});for(int i = oldStart; i < oldEnd ; i++){for(int j = newStart ; j <newEnd ;j++){int count=numJewelsInStones(oldLines.get(i),newLines.get(j));samCharMap.put(count,String.valueOf(i)+":"+String.valueOf(j));}}int i=0;String lineInfo;Iterator<Integer> it = samCharMap.keySet().iterator();while (i<n && it.hasNext()){lineInfo = samCharMap.get(it.next());String[] lineNumA=lineInfo.split(":");resultMap.put(Integer.valueOf(lineNumA[0]),Integer.valueOf(lineNumA[1]));i++;}return resultMap;}//分析文本的变化类型,存入结果集合中public static void analChangeType() {//下面开始分析差异区域的变化类型,然后按照类型进行处理//oldEnd、oldStart、newEnd、newStart为实例属性,//分别表示旧文本差异区域的起终点和新文本差异区域的起终点int oldNumDiff = oldEnd-oldStart;int newNumDiff = newEnd-newStart;//纯修改if(oldNumDiff == newNumDiff){int number=oldEnd-oldStart;for(int i = 0 ;i<number ;i++){updateLines.put(oldStart+i,newStart+i);}}else if(oldNumDiff > newNumDiff){if(newEnd==newStart){//纯删除for(int i=oldStart;i<oldEnd;i++) {//取出被删除的行,存入集合delLines.add(i);}}else {//删除加修改//计算修改的行数int updateNum=newNumDiff;//获取修改的行,getUpdateLines为获取修改行对于关系的方法,// 返回的Map中存储的是修改前后的行号Map<Integer, Integer> changeLineMap=getUpdateLines(updateNum);updateLines.putAll(changeLineMap);//获取删除的行for(int lineNum = oldStart ;lineNum <oldEnd ; lineNum ++){if(!changeLineMap.containsKey(lineNum)){delLines.add(lineNum);}}}}else {if(oldEnd==oldStart){//纯新增for(int i=newStart;i<newEnd;i++) {addLines.add(i);}}else {//新增加修改//此时修改的行数是:int number=oldNumDiff;//获取修改的旧文本行号与新文本行号组成键值对的集合Map<Integer, Integer> changeLineMap = getUpdateLines(number);updateLines.putAll(changeLineMap);//获取新增的行for(int lineNum = newStart ;lineNum <newEnd ; lineNum ++){if(!changeLineMap.values().contains(lineNum)){addLines.add(lineNum);}}}}}//递归对比文本public static void compare(){//如果能找到差异区域的起点if(getDifferenceAreaStart()){//也能找到差异区域的终点if(getDifferenceAreaEnd()){analChangeType();compare();}else {//如果找不到差异区域的终点,说明从起点开始下文全是差异区域oldEnd = oldLines.size();newEnd = newLines.size();analChangeType();}}}
}

程序计算结果测试

这是用于对比的初始文本(即代码中的“F://comparetest/1.txt”):

汉皇重色思倾国,
御宇多年求不得。
杨家有女初长成,
养在深闺人未识。
天生丽质难自弃,
一朝选在君王侧。
回眸一笑百媚生,
六宫粉黛无颜色。
春寒赐浴华清池,
温泉水滑洗凝脂。
侍儿扶起娇无力,
始是新承恩泽时。
云鬓花颜金步摇,
芙蓉帐暖度春宵。
春宵苦短日高起,
从此君王不早朝。

我们将其修改为(即代码中的“F://comparetest/2.txt”):

汉皇重色思倾国,
美人多年求不得。
杨家有女初成年,
养在深闺人未识。
天生丽质难自弃,
回头一笑百媚生,
六宫粉黛无颜色。
春寒赐浴华清池,
温泉水滑洗凝脂。
侍儿扶起娇无力,
始是新承恩泽时。
后宫佳丽三千人,
三千宠爱在一身。
云鬓花颜摇啊摇,
芙蓉帐暖度春宵。
春宵苦短日高起,
从此君王不上朝。

控制台打印的对比结果是(结果准确无误):

用Java语言实现文本内容对比的算法和程序相关推荐

  1. 易语言读文本内容_易读性如何使文本易于阅读

    易语言读文本内容 Your first step in making your texts legible is to understand what legibility means. It is ...

  2. Java实现将文本内容、网址链接url,生成二维码与反解析

    2019独角兽企业重金招聘Python工程师标准>>> Java实现将文本内容.网址链接url,生成二维码与反解析 QR码的"QR"是Quick Response ...

  3. java 读取文件文本内容_Java读取文本文件

    java 读取文件文本内容 There are many ways to read a text file in java. Let's look at java read text file dif ...

  4. JAVA——写入指定文本内容(字符)

    #JAVA--写入指定文本内容(字符) 1.要求 以文本方式向某一指定路径指定文件名的文本文件写入指定文本内容. 2.方法 WriteFileByBytes()方法以字节为单位将内容写到文件中.通过F ...

  5. java存款程序_ATM 用java语言做的一个模拟ATM机的程序。可支持取款存款等操作 Develop 238万源代码下载- www.pudn.com...

    文件名称: ATM下载 收藏√  [ 5  4  3  2  1 ] 开发工具: Java 文件大小: 865 KB 上传时间: 2015-04-13 下载次数: 0 提 供 者: 葛宏涛 详细说明: ...

  6. 我们一起来排序——使用Java语言优雅地实现常用排序算法

    破阵子·春景 燕子来时新社,梨花落后清明. 池上碧苔三四点,叶底黄鹂一两声.日长飞絮轻. 巧笑同桌伙伴,上学径里逢迎. 疑怪昨宵春梦好,元是今朝Offer拿.笑从双脸生. 排序算法--最基础的算法,互 ...

  7. JAVA语言中 文本框类的类名是_这是什么?

    [简答题]设计一个Printer类继承Output和Product接口,实现数据的获取和输出 (25.0分) [多选题]一般Java程序的类主体由哪两部分组成( ). [多选题]如果子类中的( )与父 ...

  8. Java实现替换文本内容(二)

    上一篇写的是将内容写入新文件,读完后删除源文件,在修改新文件名. 本次用的方法是创建随机文件流,用来读取源文件内容,找到第一次包含目标字符串内容的位置,然后将目标字符串替换写入临时文件,后面不包含目标 ...

  9. JAVA IO修改文本内容

    为什么80%的码农都做不了架构师?>>>    使用JD-GUI反编译Jar源码的时候,点击Save all source,保存为zip包,而包中的源码每行都多出了注释/** */ ...

最新文章

  1. 告别2019,展望2020:让我们看一看这十年中深度学习的经典瞬间
  2. “云时代架构”经典文章阅读感想十二
  3. phpstorm8 license key
  4. 百练OJ:2973:Skew数
  5. 【spring boot】【thymeleaf】SPEL处理 null 值
  6. Struts 体系结构与工作原理(图)
  7. 你知道void和Void的区别吗
  8. 物联网未来发展的十大趋势
  9. Atitit.web预览播放视频的总结
  10. 电力系统潮流计算程序 matlab,大神们,求个电力系统潮流计算的matlab程序。
  11. Openlayer:学习笔记之简单的ol.View应用
  12. jar命令成功完成 java -jar 命令却提示“没有主清单属性”!
  13. linux informix数据库下载,informix数据库基础下载_informix数据库基础官方下载-太平洋下载中心...
  14. matlab 泊松分布作图,matlab用一组数据画泊松分布图
  15. python wmi 显卡型号_确定通过WMI运行的GPU
  16. DSP-EALLOW和EDIS
  17. String的底层分析 (学习笔记)
  18. 【成长经历】----陪女朋友拔智齿
  19. opengl绘制金字塔
  20. html打印页标题行,打印标题行的设置方法

热门文章

  1. 基于FPGA的光口通信开发案例|基于Kintex-7 FPGA SFP+光口的10G UDP网络通信开发案例
  2. Android Studio修改工程项目编码,修正运行程序乱码问题
  3. StringPool详解
  4. 舔狗不会永远舔你的爱答不理和高冷
  5. ftp 21端口被占用解决办法
  6. DDR VTT供电解决方案
  7. 绝对值编码器与增量式编码器简析
  8. MSE(MeanSquaredError) loss 与 CE(CrossEntropyLoss) loss
  9. 技术博客变成情感博客了吧
  10. 学而思总裁曹允东谈创业:融资意识很重要