1. 程式人生 > >記一次用Java Stream Api的經歷

記一次用Java Stream Api的經歷

最近有個專案需要用到推薦系統,弄了個簡單的相似度推薦演算法。

資料為:

化簡為:

public class Worker {
    /**
     * 使用者編號
     */
    private long userId;
    /**
     * 期望城市
     */
    private String expectedCity;
    /**
     * 現在狀態
     */
    private int status;
    /**
     * 最高學歷
     */
    private String education;
    /**
     * 工作經驗
     */
    private int experience;
    /**
     * 星座
     */
    private String constellation;
    /**
     * 年齡
     */
    private int age;
    /**
     * 籍貫
     */
    private String nativePlace;
    /**
     * 自我介紹
     */
    private String introduction;
    /**
     * 所在地區
     */
    private String location;
    /**省略get() set()**/
}

計算策略是:

1、數值越接近,值越大

2、數值相同,返回1,否則返回0

3、如果是與字串有關的,例如(做飯做衛生,輔帶寶寶,做飯好吃,做事麻利,為人乾淨利落 ,形象好。易溝通。有育嬰師證。),則計算餘弦距離,在這裡沒有做分詞,因此將此內容的比重下降

演算法如下:

public class ScoreCos {
    /**
     * 不分詞 純字串計算
     * @param text1
     * @param text2
     * @return
     */
    public static double similarScoreCos(String text1, String text2){
        if(text1 == null || text2 == null){
            //只要有一個文字為null,規定相似度分值為0,表示完全不相等
            return 0.0;
        }else if("".equals(text1)&&"".equals(text2)) return 1.0;
        Set<Integer> ASII=new TreeSet<>();
        Map<Integer, Integer> text1Map=new HashMap<>();
        Map<Integer, Integer> text2Map=new HashMap<>();
        for(int i=0;i<text1.length();i++){
            Integer temp1=new Integer(text1.charAt(i));
            if(text1Map.get(temp1)==null) text1Map.put(temp1,1);
            else text1Map.put(temp1,text1Map.get(temp1)+1);
            ASII.add(temp1);
        }
        for(int j=0;j<text2.length();j++){
            Integer temp2=new Integer(text2.charAt(j));
            if(text2Map.get(temp2)==null) text2Map.put(temp2,1);
            else text2Map.put(temp2,text2Map.get(temp2)+1);
            ASII.add(temp2);
        }
        double xy=0.0;
        double x=0.0;
        double y=0.0;
        //計算
        for (Integer it : ASII) {
            Integer t1=text1Map.get(it)==null?0:text1Map.get(it);
            Integer t2=text2Map.get(it)==null?0:text2Map.get(it);
            xy+=t1*t2;
            x+=Math.pow(t1, 2);
            y+=Math.pow(t2, 2);
        }
        if(x==0.0||y==0.0) return 0.0;
        return xy/Math.sqrt(x*y);
    }


    /**
     * 相同返回1,不同返回0
     * @param o1
     * @param o2
     * @return
     */
    public static double equal(Object o1,Object o2) {
        return (o1!=null && o2!=null)&&o1.equals(o2)?1:0;
    }

    /**
     * 值約接近,返回值越接近1
     * 演算法為 1-(大-小)/(最大-最小)
     * @param o1
     * @param o2
     * @return
     */
    public static double similarByNumber(int o1, int o2, int max) {
        return 1-Math.abs(o1-o2)/max;
    }
}

演算法大致如下:

1、先從excel獲取資料

2、用兩個for迴圈計算物品間的相似度

3、排序後取前10個最大的

4、儲存資料

第一次跑,以工作人員的自我介紹作為相似度判斷依據

        //資料結構

        //結果
        Map<Long,List<Node>> map = new HashMap<>();

        //結果的每一行
        Map<Long, Double> row = new HashMap<>();

        //檔案內容
        Map<Long, String> content = new HashMap<>();

        //讀取檔案
        File file = new File("d:/data7.xls");
        InputStream inputStream = new FileInputStream(file);
        Workbook workbook = ExcelUtil.getWorkbok(inputStream,file);
        Sheet sheet = workbook.getSheetAt(0);

        //跳過第一個
        for (int i = 1; i < sheet.getLastRowNum(); i++) {
            Row r = sheet.getRow(i);
            Cell id = r.getCell(9);
            Cell cont = r.getCell(5);
            content.put(Long.valueOf(id.getStringCellValue()), cont.getStringCellValue());
        }
//        System.out.println(content);

        //兩個for迴圈計算相似度,取前10個
        for (Map.Entry<Long,String> c1:content.entrySet()) {
            Map<Long, Double> m = new HashMap<>();
            for (Map.Entry<Long,String> c2:content.entrySet()) {
                if(c1.getKey().equals(c2.getKey())) continue;
                double r = ScoreCos.similarScoreCos(c1.getValue(), c2.getValue());
                m.put(c2.getKey(), r);
            }
            List<Map.Entry<Long,Double>> list = new ArrayList<Map.Entry<Long,Double>>(m.entrySet());
            Collections.sort(list,new MyComparator());
            List<Node> nodeList = new ArrayList<>();
            for(int i = 0; i< 10 && i < list.size(); i++){
                Map.Entry<Long, Double> entry = list.get(i);
                nodeList.add(new Node(entry.getKey(), entry.getValue()));
            }
            map.put(c1.getKey(), nodeList);
            log.info("key:{},value:{}",c1.getKey(),nodeList);
        }


        //儲存為檔案
        save(map);

結果跑了4個小時左右,資料大概有30000個。

推測大概有如下原因:

1、單執行緒

2、只要取前10個,用不著全排序

將單執行緒變成多執行緒有多種方法。其中較為簡便的可以用Java1.8提供的並行流處理(parallelStream)

同時,從多個方面進行判斷

    /**
     * 值越接近1表示越接近
     * @param o
     * @return
     */
    public double distinct(Worker o){
        double dis = 0;
        dis += (3d / 16) * equal(this.expectedCity, o.expectedCity);
        dis += (1d / 16) * equal(this.status, o.status) ;
        dis += (2d / 16) * similarByNumber(this.experience,o.experience,496);
        dis += (2d / 16) * equal(this.education, o.education);
        dis += (2d / 16) * equal(this.constellation, o.constellation);
        dis += (2d / 16) * similarByNumber(this.age,o.age,40);
        dis += (2d / 16) * equal(this.nativePlace, o.nativePlace);
        dis += (1d / 16) * similarScoreCos(this.introduction, o.introduction);
        dis += (1d / 16) * similarScoreCos(this.location, o.location);
        return dis;
    }

改進後:

    /**
     * 流處理
     * @throws IOException
     */
    private static void useStreamApi() throws IOException {
        List<Worker> data = getFromDB();

        Map<Long, List<Node>> map = new ConcurrentHashMap<>();
        AtomicInteger integer = new AtomicInteger();
        //併發執行
        data.parallelStream().forEach(x->{
            //相當於兩個for迴圈
            List<Node> nodes = data.stream()
                //如果userId相同,則置為0
                .map(y -> new Node(y.getUserId(), x.getUserId()==y.getUserId()?0:x.distinct(y)))
                //降序
                .sorted(Comparator.reverseOrder())
                //取前10個
                .limit(10)
                //.peek(System.out::println)
                .collect(Collectors.toList());
            map.put(x.getUserId(), nodes);
            //每隔100個輸出一次
            if(integer.getAndIncrement()%100==0)
                log.info("key:{} value:{}",x.getUserId(),nodes);
        });
        save(map);
    }

重新計算一遍後用了40分鐘左右便出來了,而且stream用的也很簡潔。

參考:

《寫給大忙人看的Java SE 8》第二章