【算法】哈希摘要算法,CRC冗餘算法,MD摘要算法,HashMaker源碼分享

效果圖

這裏寫圖片描述
JavaFX部分的代碼就不發了,網上關於java圖形化界面的開發教程太少,而且開發有一定的難度(B站只有4個視頻教程,還有一個看着感覺不錯的講的是印度咖喱味的英語),接下來就只分享核心算法

算法部分用了java的很多新特性,Stream,集合工廠,這兩個新特性完全把for循環消滅在一行代碼中,簡化了項目

源碼和工具包用法什麼的,請前往github,https://github.com/ZDG-Kinlon/HashMaker

純JAVA基本變量的代碼實現,待資料收集完畢後更新

至於每個版本支持哪些摘要,可以參考給出的算法遍歷 http://bbs.csdn.net/topics/80316868

Algorithm.java

package cn.hash;

public interface Algorithm {
    //冗餘算法
    String CRC32 = "CRC32";//java 6
    String CRC32C = "CRC32C";//java 9

    //MD摘要
    String MD2 = "MD2";//java 6
    String MD5 = "MD5";//java 6

    //SHA1摘要
    /*String SHA = "SHA";*/
    String SHA_1 = "SHA-1";//java 6

    //SHA2摘要
    String SHA_224 = "SHA-224";//java 8
    String SHA_256 = "SHA-256";//java 6
    String SHA_384 = "SHA-384";//java 6
    String SHA_512 = "SHA-512";//java 6
    String SHA_512_224 = "SHA-512/224";//java 9
    String SHA_512_256 = "SHA-512/256";//java 9

    //SHA3摘要
    String SHA3_224 = "SHA3-224";//java 9
    String SHA3_256 = "SHA3-256";//java 9
    String SHA3_384 = "SHA3-384";//java 9
    String SHA3_512 = "SHA3-512";//java 9
}

Method.java

計算摘要所需的方法都在這裏,使用Lambda表達式,將方法傳遞給控制類,在控制類中傳入參數調用進行計算

方法分爲初始化參數、計算、結果整理三步驟,由於大型文件在計算時,需要分段截取字節然後進行計算,計算過程中必須按照一定順序進行偏移,所以不支持多線程,在多線程的情況下,不確定哪個線程會先執行,導致順序錯誤,生成錯誤的摘要值

package cn.hash;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.zip.CRC32;
import java.util.zip.CRC32C;
import java.util.zip.Checksum;

public class Method {
    //待計算的任務集合
    private Set<String> typeSet;
    //遍歷需要計算指定摘要類型的set集合,key=摘要類型,value=對應摘要類型的摘要對象
    private Map<String, MessageDigest> mapMD = new HashMap<>();
    //遍歷需要計算指定冗餘類型的set集合,key=冗餘類型,value=對應冗餘類型的冗餘對象
    private Map<String, Checksum> mapCS = new HashMap<>();

    public Runnable getMethod1() {
        return method1;
    }

    public BiConsumer<byte[], Integer> getMethod2() {
        return method2;
    }

    public Function<Boolean, Map<String, String>> getMethod3() {
        return method3;
    }

    public Method(Set<String> typeSet) {
        this.typeSet = typeSet;
    }

    //初始化方法的Lambda表達式
    private Runnable method1 = () -> {
        //創建摘要對象集合
        typeSet.forEach(type -> {
            switch (type.toUpperCase()) {
                case "CRC32":
                    //crc32算法
                    mapCS.put(type, new CRC32());
                    break;
                case "CRC32C":
                    //crc32c算法
                    mapCS.put(type, new CRC32C());
                    break;
                default:
                    try {
                        mapMD.put(type, MessageDigest.getInstance(type));
                    } catch (NoSuchAlgorithmException e) {
                        e.printStackTrace();
                        //不支持的算法跳過
                        mapMD.put(type, null);
                    }
                    break;
            }
        });
    };

    //計算方法
    private BiConsumer<byte[], Integer> method2 = (a, b) -> {
        typeSet.forEach(type -> {
            switch (type.toUpperCase()) {
                case "CRC32":
                case "CRC32C":
                    //冗餘算法
                    mapCS.get(type).update(a, 0, b);
                    break;
                default:
                    //摘要算法,沒有得到對象的直接跳過
                    if (mapMD.get(type) != null) {
                        //摘要值生成,分段更新
                        mapMD.get(type).update(a, 0, b);
                    }
                    break;
            }
        });
    };

    //結果方法的Lambda表達式
    private Function<Boolean, Map<String, String>> method3 = (a) -> {
        //結果的map集合,key=類型,value=值
        Map<String, String> resultMap = new HashMap<>();
        typeSet.forEach(type -> {
            String resultStr = null;
            switch (type.toUpperCase()) {
                case "CRC32":
                case "CRC32C":
                    resultStr = dec2hex(mapCS.get(type).getValue(), a);
                    break;
                default:
                    if (mapMD.get(type) != null) {
                        resultStr = bts2Hex(mapMD.get(type).digest(), a);
                    }
                    break;
            }
            resultMap.put(type, resultStr);
        });
        return resultMap;
    };

    /**
     * 任意進制的數字轉換[2-36]
     *
     * @param num         待轉換的數字
     * @param fromType    待轉換數字的進制類型
     * @param toType      目標數字的進制類型
     * @param isLowerCase 目標大小寫控制
     * @return 字符串數字
     * @deprecated 執行效率原因,不到萬不得已不推薦使用
     */
    @Deprecated
    public String numTranslate(String num, int fromType, int toType, boolean isLowerCase) {
        num = new BigInteger(num, fromType).toString(toType);
        return isLowerCase ? num.toLowerCase() : num.toUpperCase();
    }

    /**
     * 十進制轉十六進制
     *
     * @param num         十進制待轉換的數字
     * @param isLowerCase 目標大小寫控制
     * @return 十六進制的數字
     */
    public String dec2hex(Long num, boolean isLowerCase) {
        String str = Long.toHexString(num);
        return isLowerCase ? str.toLowerCase() : str.toUpperCase();
    }

    /**
     * 將字節數組轉換爲十六進制字符串的結果
     *
     * @param bytes       摘要字節數組
     * @param isLowerCase 目標結果大小寫
     * @return 32位的十六進制摘要結果,如果需要16位的MD5摘要,substring(8, 24)截取即可
     */
    public String bts2Hex(byte[] bytes, boolean isLowerCase) {
        List<Character> table;
        if (isLowerCase) {
            table = List.of('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f');
        } else {
            table = List.of('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F');
        }
        int length = bytes.length;
        char[] temp = new char[length << 1];
        int i = 0;
        int var = 0;
        while (i < length) {
            temp[var++] = table.get((240 & bytes[i]) >>> 4);
            temp[var++] = table.get(15 & bytes[i++]);
        }
        return new String(temp);
    }
}

MsgDigestFile.java

計算單個文件的摘要計算類,構造方法傳入需要計算的摘要類型的Set集合,大小寫輸出方式,File對象,調用hash方法即可返回Map集合,key爲摘要算法類型,value爲摘要值

緩衝區的設置的小了,節省內存,讀取操作會增加,效率會降低,設置大了,內存會佔用很多,但是讀取操作會減少,一口氣全部加載到內存條然後完成計算是最佳效率,但是偏移最大隻能爲2GB(int範圍限制),所以超過2G的文件還是別想能全部加載到內存中,分段讀取到內存中計算是可行

預留了多線程方法,因爲有返回值,沒有使用實現Runnable接口的方式來實現多線程,使用線程池會更容易管理線程

package cn.hash;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;

public class MsgDigestFile implements Callable<Map<String, String>> {
    //方法對象
    private Method method;

    private Map<String, String> map;
    private StringBuffer sb;
    private int fileCacheSize = 1048576;//1MB
    private boolean isLowerCase;

    //文件
    private File file;
    //文件大小,字節
    private long fileSize;
    //剩餘未讀取字節數
    private long surplusFileSize;

    public MsgDigestFile(Set<String> set, boolean isLowerCase, File file) {
        this.isLowerCase = isLowerCase;
        this.file = file;

        this.sb = new StringBuffer();
        this.method = new Method(set);
        surplusFileSize = fileSize = file.length();
        sb.append("文件:").append(file.getAbsolutePath()).append("\n");
        sb.append("大小:").append(file.length()).append(" 字節\n");
    }


    /**
     * 線程池方法
     *
     * @return 執行結果
     * @throws Exception
     */
    @Override
    public Map<String, String> call() throws Exception {
        return hash();
    }

    /**
     * 單線程方法
     *
     * @return
     */
    public Map<String, String> hash() {
        //創建緩衝區
        byte[] bytes = new byte[fileCacheSize];
        //初始化方法對象
        method.getMethod1().run();
        //創建文件輸入流,自動關閉資源
        try (FileInputStream fis = new FileInputStream(file)) {
            //讀取文件字節到緩存,在緩存中進行計算
            while (fis.read(bytes) != -1) {
                //計算偏移量,bytes中有效的未計算的字節長度
                int offset = surplusFileSize > fileCacheSize ? fis.available() != 0 ? fileCacheSize : (int) surplusFileSize : (int) surplusFileSize;
                //計算所有的任務
                method.getMethod2().accept(bytes, offset);
                //更新剩餘未讀取的字節
                surplusFileSize -= offset;
            }
            //生成結果
            return map = method.getMethod3().apply(isLowerCase);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 結果集合
     *
     * @return
     */
    public Map<String, String> getMap() {
        return map;
    }

    @Override
    public String toString() {
        map.forEach((k, v) -> sb.append(k).append(":").append(v).append("\n"));
        return sb.toString();
    }

    /**
     * 文件字節總數
     *
     * @return
     */
    public long getFileSize() {
        return fileSize;
    }

    /**
     * 剩餘未讀取的字節數
     *
     * @return
     */
    public long getSurplusFileSize() {
        return surplusFileSize;
    }

    /**
     * 設置緩衝區大小,字節,默認1MB
     *
     * @param fileCacheSize
     */
    public void setFileCacheSize(int fileCacheSize) {
        if (fileCacheSize > 0) {
            this.fileCacheSize = fileCacheSize;
        }
    }

    /**
     * 獲取文件對象
     *
     * @return
     */
    public File getFile() {
        return file;
    }
}

MsgDigestFiles.java

基於上面的類,拓展出多文件的摘要計算類,由於每個文件使用的是一個MsgDigestFile對象,不存在同步的問題,完全可以使用線程來提高效率,使用的是生成固定個數線程的線程池,這個個數根據文件來決定,所有文件一起同時啓動爭搶CPU資源進行計算,讓CPU滿負荷

package cn.hash;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class MsgDigestFiles {

    //基礎變量
    private Set<String> set;
    private Map<File, Map<String, String>> resultMap;
    private StringBuffer sb;
    private int fileCacheSize = 1048576;//1MB
    private boolean isLowerCase;

    //文件集合
    private List<File> fileList;
    //文件個數
    private int fileCount;
    //文件總大小,字節
    private long filesSize;

    public MsgDigestFiles(Set<String> set, boolean isLowerCase, List<File> fileList) {
        this.set = set;
        this.isLowerCase = isLowerCase;
        this.fileList = fileList;

        this.fileCount = fileList.size();
        this.resultMap = new HashMap<>();
        this.sb = new StringBuffer();
    }

    /**
     * 多線程方法
     */
    public Map<File, Map<String, String>> hash() {
        filesSize = 0;
        ExecutorService threadPool = Executors.newFixedThreadPool(fileCount);
        List<MsgDigestFile> msgDigestFileList = new ArrayList<>();
        Map<File, Future<Map<String, String>>> fileFutureMap = new HashMap<>();
        fileList.forEach(file -> {
            filesSize += file.length();
            MsgDigestFile msgDigestFile = new MsgDigestFile(set, isLowerCase, file);
            msgDigestFile.setFileCacheSize(fileCacheSize);
            msgDigestFileList.add(msgDigestFile);
        });
        try {
            msgDigestFileList.forEach(msgDigestFile -> fileFutureMap.put(msgDigestFile.getFile(), threadPool.submit(msgDigestFile)));
        } finally {
            threadPool.shutdown();
        }
        fileFutureMap.forEach((file, mapFuture) -> {
            try {
                resultMap.put(file, mapFuture.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
        return resultMap;
    }

    @Override
    public String toString() {
        resultMap.forEach((file, map) -> {
            sb.append("文件:").append(file.getAbsolutePath()).append("\n").append("大小:").append(file.length()).append("\n");
            map.forEach((k, v) -> sb.append(k).append(":").append(v).append("\n"));
        });
        return sb.toString();
    }

    public long getFilesSize() {
        return filesSize;
    }
}

MsgDigestString.java

以上都是文件的摘要計算,接下來是字符串的摘要計算,由於字符串長度不會太長(最大理論支持到2GB,再大的只能存到文本文件裏,然後用上面的方法去處理),不進行多線程方法的實現,也不借助緩衝區,這個類只需要把字符串用指定的字符集拆分爲字節數組,傳入進行計算即可獲得結果

package cn.hash;

import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.Set;

public class MsgDigestString {
    //方法對象
    private Method method;

    //基礎變量
    private Map<String, String> map;
    private StringBuffer sb;
    private boolean isLowerCase;

    //字符串
    private String string;
    //字符編碼類型
    private String charset = "utf-8";

    public MsgDigestString(Set<String> set, boolean isLowerCase, String string) {
        this.isLowerCase = isLowerCase;
        this.string = string;

        this.sb = new StringBuffer();
        this.method = new Method(set);
        sb.append("文本:").append(string).append("\n");
        sb.append("字數:").append(string.length()).append(" 字節\n");
    }

    public Map<String, String> hash() {
        //初始化方法對象
        method.getMethod1().run();
        //創建字符流,自動關閉資源
        try {
            method.getMethod2().accept(string.getBytes(charset), string.length());
            //生成結果
            return this.map = method.getMethod3().apply(isLowerCase);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public String toString() {
        map.forEach((k, v) -> sb.append(k).append(":").append(v).append("\n"));
        return sb.toString();
    }

    /**
     * 字符串編碼,缺省爲默認爲utf-8,不識別的字符集默認utf-8
     *
     * @param charset
     */
    public void setCharset(String charset) {
        try {
            "".getBytes(charset);
            this.charset = charset;
        } catch (UnsupportedEncodingException e) {
            this.charset = "utf-8";
        }
    }
}

最後就是調用示例

package cn.demo;

import cn.hash.MsgDigestFile;
import cn.hash.MsgDigestFiles;
import cn.hash.Algorithm;
import org.junit.Test;

import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class FileDemo {

    /**
     * 一個文件執行的示例
     */
    @Test
    public void fileDemo1() {
        //1.創建文件對象
        File file = new File("D:\\1.png");
        //2.準備需要執行的方法集合,支持的類型在Algorithm接口中
        Set<String> set = Set.of(
                Algorithm.CRC32, Algorithm.CRC32C,
                Algorithm.MD2, Algorithm.MD5,
                Algorithm.SHA_1,
                Algorithm.SHA_224, Algorithm.SHA_256, Algorithm.SHA_384, Algorithm.SHA_512, Algorithm.SHA_512_224, Algorithm.SHA_512_256,
                Algorithm.SHA3_224, Algorithm.SHA3_256, Algorithm.SHA3_384, Algorithm.SHA3_512
        );
        //3.創建摘要對象,傳入摘要類型,大小寫輸出,文件
        MsgDigestFile msgDigestFile = new MsgDigestFile(set, false, file);
        //5.設置緩衝區的大小,單位字節,默認1MB,一次性讀取1MB,分段處理大型文件
        msgDigestFile.setFileCacheSize(10485760);//10MB
        msgDigestFile.getFileSize();//文件的大小,單位字節,long
        msgDigestFile.getSurplusFileSize();//剩餘未讀取的字節大小,單位字節,long
        //6.執行,接收結果,key=算法類型,value=計算的摘要值
        Map<String, String> map = msgDigestFile.hash();
        //7.輸出結果
        map.forEach((key, value) -> System.out.println(key + ":" + value));
        String string = msgDigestFile.toString();//字符串的結果,包括文件路徑,文件大小,和各文件摘要結果信息(無序),換行輸出
        System.out.println(string);
    }

    /**
     * 多文件,多線程的示例
     */
    @Test
    public void fileDemo2() {
        //1.創建文件集合
        List<File> fileList = List.of(
                new File("D:\\1.png"),
                new File("D:\\2.png"),
                new File("D:\\3.png")
        );
        //2.準備需要執行的方法集合,支持的類型在Algorithm接口中
        Set<String> set = Set.of(
                Algorithm.CRC32, Algorithm.CRC32C,
                Algorithm.MD2, Algorithm.MD5,
                Algorithm.SHA_1,
                Algorithm.SHA_224, Algorithm.SHA_256, Algorithm.SHA_384, Algorithm.SHA_512, Algorithm.SHA_512_224, Algorithm.SHA_512_256,
                Algorithm.SHA3_224, Algorithm.SHA3_256, Algorithm.SHA3_384, Algorithm.SHA3_512
        );
        //3.創建多線程的摘要對象,傳入摘要類型,大小寫輸出,文件list集合
        MsgDigestFiles msgDigestFiles = new MsgDigestFiles(set, false, fileList);
        //4.獲取結果
        Map<File, Map<String, String>> map = msgDigestFiles.hash();
        //5.輸出結果
        map.forEach((f, m) -> {
            System.out.println("文件:" + f.getAbsolutePath());
            System.out.println("大小:" + f.length());
            m.forEach((k, v) -> System.out.println(k + ":" + v));
        });
        System.out.println("======");
        String string = msgDigestFiles.toString();//字符串的結果,包括文件路徑,文件大小,和各文件摘要結果信息(無序),換行輸出
        System.out.println(string);
    }
}

結果:
單文件:
這裏寫圖片描述

超大文件:(21.4GB)
這裏寫圖片描述

多文件,多線程:
這裏寫圖片描述

字符串:
這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章