Java编程记录 --- 工具箱(填充中...)

本文介绍了Java编程中一些实用的技巧和实践经验,包括字符串处理、时间戳获取、百分比数据展示、数据库表与对象映射、Java反射机制、加密解密、文件操作等方面的内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本篇是记录自己在学习中遇到一些用法(比较泛,只要是我认为比较奇怪,惊艳的,有价值的都可以)。还有一些我经常会用到的和一些有趣的封装,先记录吧,可能后面多了会很乱,到时再说。

字符串

null字符串

这是一个很有意思的现象,我到现在才知道,原来会出现

String name = null;
System.out.println(name+"");
System.out.println(""+null); //"null"
System.out.println(String.valueOf(name)); //"null"

"null"这样的字符串。无论是""+null还是String.valueOf(name),Object name=null都会形成“null”字符串,而""+null的写法比较简洁,不会太长,所以一般我喜欢用前面一种(个人喜好)。

这里说这个现象的目的是:在判断字符串的时候,不要只考虑两种情况,还有第三种:

  1. 字符串为null
  2. 字符串为空字符串
  3. 字符串为"null"(null字符串)
import org.apache.commons.lang.StringUtils;

String name = String.valueOf((...从request中获得请求参数)+"")
if(StringUtils.isEmpty(name) && "null".equals(name)){
    ...
}

顺便说一下字符串判空的常用方法:

String s = "";
String s1 = " ";
String s2 = null;
String s3 = ""+null;

System.out.println(StringUtils.isEmpty(s)); //true
System.out.println(StringUtils.isEmpty(s1)); //false
System.out.println(StringUtils.isEmpty(s2)); //true
System.out.println(StringUtils.isEmpty(s3)); //false

//StringUtils.isEmpty()
public static boolean isEmpty(String str) {
    return str == null || str.length() == 0;
}

//StringUtils.isNoEmpty()
public static boolean isNotEmpty(String str) {return !isEmpty(str);}

//com.mysql.jdbc.StringUtils.isNullOrEmpty()
public static boolean isNullOrEmpty(String toTest) {
    return (toTest== null || toTest.length() == 0);
}
  1. 可以看出StringUtils.isEmpty只是帮助我们对常用判空进行封装而已。
  2. 我看见有些人比较喜欢使用com.mysql.jdbc.StringUtils.isNullOrEmpty(),虽然本质上并没有什么不同。我猜测应该是名字起的让人放心(多了一个Null)。这说明起一个好名字也很重要啊~。

字符串分割常用方法

import org.apache.commons.lang.StringUtils

String test1 = "你好#您好#大家好";
String test2 = "你好#您好#大家好#";
String test3 = "#你好#您好#大家好#";

//String自带的split方法
String[] args1 = test1.split("#"); //{"你好", "您好", "大家好"}
String[] args2 = test2.split("#"); //{"你好", "您好", "大家好"}
String[] args3 = test3.split("#"); //{"", "你好", "您好", "大家好"}

//StringUtils的split方法
//从test1->3都是一样
String[] args4 = StringUtils.split(test3, "#"); //{"你好", "您好", "大家好"}

//StringUtils的splitByWholeSeparator方法
//{"你好", "您好", "大家好", ""}
String[] args5 = StringUtils.splitByWholeSeparator(test3, "#");

//StringUtils的splitPreserveAllTokens方法
//{"", "你好", "您好", "大家好", ""}
String[] args5 = StringUtils.splitPreserveAllTokens(test3, "#");

String url1 = "type=test&sendtime=&";

String[] args6 = StringUtils.split(url1, "&"); //{"type=test", "sendtime="}
for(int i=0; i<args6.length; i++){
    String[] args7 = StringUtils.splitPreserveAllTokens(args6[i], "=");
    System.out.println(args7[0] + ":" + args7[1]);
}
//type:test
//sendtime=

从上面的一些例子可以看出:

  1. 若是正常分割(如test1->3),不需要空字符串,推荐使用StringUtils自带的split分割方法
  2. 若是需要分解类似url的,如url1,需要sendtime后面的空字符串组成键值对,推荐使用StringUtils自带的splitPreserveAllTokens方法
  3. 总之,推荐使用StringUtils,而对于String自带的split方法要慎重使用。

判断字符串中是否含有中文

可以通过utf-8编码后的字节长度来判断:

String str = "nihao!!!"; //全英文字符
int len_str = str.length();
int len_byte = str.getBytes("utf-8").length;
System.out.println(len_str + " " + len_byte); //8 8

String str = "你好nihao!!!";
int len_str = str.length();
int len_byte = str.getBytes("utf-8").length;
System.out.println(len_str + " " + len_byte); //10 20

获取时间戳的几种方式

先写几种用过的:

import org.apache.commons.lang.time.DateFormatUtils;

DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss,SSS");

new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(System.currentTimeMillies()));

 获取n天前的时间戳(日期)

现在有个需求就是根据配置来保存某表中的n天内的数据,言下之意就是删除n天以前的数据。所以第一步,需要获得所需的时间点。而mysql库中表对应的字段类型是datetime,时间示例为2018-01-01 01:01:01。

一开始我使用的是System.currentTimeMillis()+SimpleDateFormat来获取时间点,但是犯了一个很傻的错误,为了以后铭记在心,记录这个错误:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dayUpper = sdf.format(System.currentTimeMillis() - 1000*60*60*24*30);
System.out.println(dayUpper);

//假设今天是2018-11-29 01:51:xx
//输出结果2018-11-09 08::48:34

 很明显,时间对不上,经过多次测试,发现原来是数据类型int溢出,泪流满面。。。,在30后面加上一个L就好了。

根据百度搜索,还有另外一种获取方式:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.DATE, -30);
String dayUpper = sdf.format(calendar.getTime());
System.out.println(dayUpper); //可以获取30天之前的日期

//2018-11-29 02:02:xx --- 现在
//2018-10-30 02:01:58 --- 30天前

 百分比数据的获取

一种数据显示的形式,应用场景很多:

DecimalFormat df = new DecimalFormat("0.00%");
int numberOfBoys = 10;
int total = 37;
double proportion = numberOfBoys/(double)total;
System.out.println(df.format(proportion )); //27.03%

EdutPlus的小写->大写正则替换

将双引号内的字符串全部换成大写。

引出这个需求的场景,就是如果从oracle中将数据库导出为sql文件,若是小写它会全部加上双引号,这里需要将其换成大写,手改是不可能了,只能用正则替换。

替换的字符串:类似,md5_user_info

按Ctrl+h,查找项:\"([a-zA-Z|_|0-9]*)\",替换项:\"\U\1\E\",当然下面要勾选支持正则和\U符合。


Java容器

Map中key和value为null的问题

因为之前有遇到过这个问题,后来查询资料发现并不是所有Map容器都可以为null,有一份表格(可以看看这篇博客)。

这里对常用的容器进行简单测试,加深印象:

首先是HashMap,可以看出无报错,且可以正常使用:

HashMap<String, Object> map = new HashMap<String, Object>();
map.put(null, "nullKey");
map.put("nullValue", null);

System.out.println(map.get(null)); //nullKey
System.out.println(map.get("nullValue")); //null

接下来是TreeMap,不能令key=null ,这个也比较好理解(红黑树,二叉排序树的变种):

TreeMap<String, Object> map = new TreeMap<String, Object>();
//map.put(null, "nullKey"); //报错
map.put("nullValue", null);

//System.out.println(map.get(null));
System.out.println(map.get("nullValue")); //null

然后是ConcurrentHashMap,可以看出都不行:

ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<String, Object>();
//两个都会报错
map.put(null, "nullKey");
map.put("nullValue", null);

 Map遍历常用方式

这里当然是指我比较喜欢用的:

HashMap<String, Object> tmpMap = new HashMap<String, Object>();
tmpMap.put("String", "String");
tmpMap.put("Integer1", 1);
tmpMap.put("Integer2", 2);

Iterator<Map.Entry<String, Object>> iterator = tmpMap.entrySet.iterator();
while(iterator.hasNext()){
    Map.Entry<String, Object> entry = iterator.next();
    System.out.println(entry.getKey() + "-" + entry.getValue());
}

/*
 * String-String
 * Integer1-1
 * Integer2-2
 */

 List和String[]的互相转换

今天遇到一个问题:

ArrayList<String> list = new ArrayList<String>();
list.add("1");
String[] strArray = (String[])list.toArray();
System.out.println(strArray[0]);

//报错:Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;

 报错很明显,禁止将Object[]转换为String[]。接下里复习一下以前的知识:

//test1
Object o = new Object();
String s = (String)o; //报错

//test2
Object o = "test2"; //向上转型
String s = (String)o; //向下转型
System.out.println(s); //test2

 可以看出来,想要向下转型,需要先向上转型,给出一个好理解的说法:公交车是交通工具,但是交通工具并不等于公交车。

而对于toArray的两种方法(带参和不带参):

//带参
public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    //根据传入的Class对象来构造数组
    T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
    Math.min(original.length, newLength));
    return copy;
}

//-------------------------------------------------------------------------------

//不带参
public Object[] toArray() {
    return Arrays.copyOf(elementData, size);
}

//ArrayList中存储数据的数组
transient Object[] elementData;

//Arrays的copyOf,T[]->Object[]
public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

//ArrayList构造方法
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

 可以看出,toArray()方法操作的Object[]自始至终都没有经过其他数组的向上转型,而toArray(T[])里面调用的copyOf方法会根据具体的Class对象构造数组。

 解决方法:

ArrayList<String> list = new ArrayList<String>();
list.add("1");
String[] strArray = list.toArray(new String[list.size()]);
System.out.println(strArray[0]); //1

那么将String[]转换为List的方式:

String[] s = {"a", "b", "c"};
List<String> list = Arrays.asList(s);
System.out.println(list.get(0)); //a

 


Java反射机制

数据库表和对象映射(ORM)

将从数据库中取出来的记录组装成对象,适用于mysql和oracle,简单测试过,没有发现明显异常(这里是手打的,可能会有打错的地方)。

主要思路:根据数据库表的字段类型来组装,因为oracle数据库中对于整型通常用number类型表示,所以根据精度和长度对其进行分类。然后根据不同的字段类型用对应的方法处理,获取表中字段名所对应的属性名,拼接出对应的set方法,进行调用。

public class ResultUtil{
    public static Logger log = Logger.getLogger(ResultUtil.class);

    //使用泛型,可以不用考虑对象类型
    public static <T> List<T> assemble(ResultSet rs, Class<T> bean){
        ArrayList<T> result = new ArrayList<T>():
        try{
            int r = rs.getMetaData().getColumnCount(); //数据表记录的字段数量
            
            while(rs.next()){
                T obj = bean.newInstance();
                for(int i=1; i<=r; i++){
                    switch(rs.getMetaData().getColumnTypeName(i)){ //字段类型名
                    case "INT":
                        treatAsInt(rs, obj, i);
                        break;
                    case "DOUBLE":
                        treatAsDouble(rs, obj, i);
                        break;
                    case "NUMBER":
                        treatAsNumber(rs, obj, i);
                        break;
                    case "FLOAT":
                        treatAsFloat(rs, obj, i);
                        break;
                    default: //varchar,datatime,date|varchar2,date
                        treatAsString(rs, obj, i);
                        break;
                    }
                }
                result.add(obj);
                log.info(obj);
            }
        }catch(Exception e){
            e.printStackTrace();
        }

        return result;
    }

    private static void treatAsInt(ResultSet rs, Object bean, int i) throws Exception{
        Method method = bean.getClass().getMethod("set" + firstUpperCase(rs.getMetaData().getColumnLabel(i)), Integer.TYPE);
        method.invoke(bean, new Object[]{Integer.valueOf(rs.getInt(i))});
    }

    private static void treatAsLong(ResultSet rs, Object bean, int i) throws Exception{
        Method method = bean.getClass().getMethod("set" + firstUpperCase(rs.getMetaData().getColumnLabel(i)), Long.TYPE);
        method.invoke(bean, new Object[]{Long.valueOf(rs.getInt(i))});
    }

    private static void treatAsDouble(ResultSet rs, Object bean, int i) throws Exception{
        Method method = bean.getClass().getMethod("set" + firstUpperCase(rs.getMetaData().getColumnLabel(i)), Double.TYPE);
        method.invoke(bean, new Object[]{Double.valueOf(rs.getInt(i))});
    }

    private static void treatAsFloat(ResultSet rs, Object bean, int i) throws Exception{
        Method method = bean.getClass().getMethod("set" + firstUpperCase(rs.getMetaData().getColumnLabel(i)), Float.TYPE);
        method.invoke(bean, new Object[]{Float.valueOf(rs.getInt(i))});
    }

    private static void treatAsNumber(ResultSet rs, Object bean, int i) throws Exception{
        int scale = rs.getMetaData().getScale(i);
        //表中定义的精度,即小数点后的位数,这里只要是有小数位统一使用double处理
        if(scale>0){
            treatAsDouble(rs, bean, i);
            return;
        }
        //获取表中定义的长度,小于10位统一使用int处理
        if(rs.getMetaData().getPrecision(i)<10){
            //int max_value 2147483647
            treatAsInt(rs, bean, i);
        }
        treatAsLong(rs, bean, i);
    }

    private static void treatAsString(ResultSet rs, Object bean, int i){
        Method method = bean.getClass().getMethod("set" + firstUpperCase(rs.getMetaData().getColumnLabel(i)), String.class);
        method.invoke(bean, new Object[]{rs.getString(i)});
    }

    //将字符串开头大写,这里主要用于拼接set方法
    public static String firstUpperCase(String columnName){
        return columnName.substring(0, 1).toUpperCase().concat(columnName.substring(1));
    }
}

这种方法要求数据库表的字段名要和类属性名对应,若是不一样,可以考虑使用映射方式,比如map或者配置文件,也可以通过注解来实现。

首先要定义一个注解:

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD) //声明该注解用于字段
@Retention(RetentionPolicy.RUNTIME) //使其可以通过反射读取
@Documented
public @interface Column{
    public String columnName() default ""; //注解属性,通过调用该方法可以返回对应的value
}

在 ResultUtil中增加一个映射方法,getColumnMappedName(ResultSet rs, Object bean, int i)。将上面的getColumnLable替换即可。通过遍历字段来获得所需的注解属性,最后返回映射的字段名。

    public static String getColumnMappedName(ResultSet rs, Object bean, int i) throws Exception{
        String columnName = rs.getMetaData().getColumnLabel(i); //数据库字段名
        Field[] fields = bean.getClass().getDeclaredFields();
        for(int j=0; j<fields.length; j++){
            if(fields[j].isAnnotationPresent(Column.class)){ //是否含有@Column注解
                Column column = (Column)fields[j].getAnnotation(Column.class);
                //获取注解的columnName属性值和字段名比较
                if(column.columnName().equals(columnName)){
                    return fields[j].getName(); //返回字段名
                }
            }
        }
        return columnName;
    }

 注解参考网址:https://www.cnblogs.com/huajiezh/p/5263849.html


加密解密/编码解码

32位的md5加密,全部小写(大写)

这个加密代码我是从网上查的,版本感觉都差不多,不过还有另外一种比较简洁的版本,下面会有说到。

/**
 *签名32位md5加密,全小写
 *
 */
public static String md5Convert(String str)
    throws NoSuchAlgorithmException{
    MessageDigest md5 = MessageDigest.getInstance("MD5");
    md5.update(str.getBytes("UTF-8"));
    byte b[] = md5.digest();
    int i;
    StringBuffer sf = new StringBuffer("");
    for(int offset=0; offset<b.length; offset++){
        i = b[offset];
        if(i<0){
            i=i+256;
        }
        if(i<16){
            sf.append("0");
        }
        sf.append(Integer.toHexString(i));
        return sf.toString().toLowerCase();
    }
}

使用给定的密钥进行MD5加密,32位,大写,字符串用GBK编码。

/**
 * 用给定的密钥对字符串进行32位的md5加密,全大写,字符集采用GBK
 * @param str 待加密字符串
 * @param key 密钥
 * @return
 */
public static String getSign(String str, String key){
    StringBuffer sf = new StringBuffer("");
    try {
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        md5.update(str.getBytes("GBK"));
        byte temp[] = md5.digest(key.getBytes("UTF-8"));
        for(int i=0; i<temp.length; i++){
            sf.append(Integer.toHexString((temp[i] & 0x000000FF) |
                        0xFFFFFF00).substring(6));
        }
        return sf.toString().toUpperCase();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return sf.toString().toUpperCase();
}

文件转base64字符串


JSON

生成Json数组的两种方案

生成效果

{
    "reportCard":
    [
        {
            "subject":"Math",
            "Score":60,
            "time":"2018-08-15 08:56:42",
            "teacherComment":"no"
        }
        ....
    ]
}

有两种方法,

第一种,通过Map和List生成

import.alibaba.fastjson.JSON

Map<String, Object> reportCard= new HashMap<>();
List<Map<String, Object>> list = new ArrayList<>();
Map<String, Object> elem = new HashMap<>();
elem.put("subject", "Math");
elem.put("score", 60);
elem.put("time", "Math");
elem.put("teacherComment", "no");
list.add(elem);
reportCard.put("reportCard", list);
System.out.println(JSON.toJSONString(reportCard));

另外一种,通过JSONObject和JSONArray和上面一样,JSONObject相当于map,JSONArray相当于List

将任意给定的json字符串解析成Map

解析的方式有两种,比较简便的是我最近看到的一种,另外一种是我以前写的:

第一种是通过fastjson自带的JSON进行解析,然后直接转换成Map

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("yes", "1");
        jsonObject.put("no", 2);
        JSONArray array = new JSONArray();
        JSONObject arrayItem = new JSONObject();
        arrayItem.put("name", "xck");
        array.add(arrayItem);
        jsonObject.put("list", array.toJSONString());

        String json = jsonObject.toJSONString();

        Map<String, Object> map = (Map<String, Object>) JSON.parse(json); //直接转换

        System.out.print(map);

        //{"yes":"1","no":2,"list":"[{\"name\":\"xck\"}]"}

第二种,是我手动解析的方式。为了满足这样的需求,考虑使用Map<String,Object>来存储。

解析json字符串可能会遇到四种类型:JSONObject,JSONArray,Integer和String。而数组里面又有可能存储json对象,所以考虑使用递归解决。

    /**
     * 将任意给定json字符串解析为map返回
     * 类型转换: jsonArray->List<Map>,jsonObj->Map,Integer/String->key-value
     * @param jsonStr
     * @return
     */
    public static Map<String, Object> readJsonToMap(String jsonStr){
        Map<String, Object> jsonMap = new HashMap<String, Object>();
        JSONObject jsonObject = JSON.parseObject(jsonStr);
        Iterator<Map.Entry<String, Object>> objIterator = jsonObject.entrySet().iterator();
        while(objIterator.hasNext()){
            Map.Entry<String, Object> entry = objIterator.next();
            Object obj = entry.getValue();
            if(obj instanceof JSONObject){
                JSONObject child = (JSONObject) obj;
                jsonMap.put(entry.getKey(), readJsonToMap(child.toJSONString())); //递归
            } else if(obj instanceof JSONArray){ //数组
                JSONArray jsonArray = (JSONArray) obj;
                Iterator arrayIterator = jsonArray.iterator();
                List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
                while(arrayIterator.hasNext()){
                    list.add(readJsonToMap(arrayIterator.next().toString())); //递归
                }
                jsonMap.put(entry.getKey(), list);
            } else {
                //obj instanceof Integer || obj instanceof String 处理基本类型
                jsonMap.put(entry.getKey(), obj);
            }
        }

        return jsonMap;
    }

 

 注意

  • 在解析json时候,最好进行try-catch进行异常处理,因为有很多情况,并不能保证解析的字符串是json字符串;

随机数

生成n位随机数

这是别人写的标题是链接,简单说一下原理,一开始现将,[0,1)*9+1=[1,10),然后根据需要将[1,10)*10^(n-1)。

/**
 * 生成n位随机数
 * @param n 随机数的长度
 * @return
 */
public static long getNDigitRanNum(int n){
    if(n < 1){
        throw new IllegalArgumentException("随机数位数必须大于0");
    }
    return (long)(Math.random()*9*Math.pow(10,n-1)) + (long)Math.pow(10,n-1);
}

自定义范围的随机数

/**
 * 生成指定上下界的随机数
 * [0, maxBound)%range=[0, range)+minBound=[minBound, minBound+range)
 * @param minBound 下界
 * @param maxBound 上界
 * @return
 */
public static int getRandomNumInRange(int minBound, int maxBound){
    Random random = new Random();
    return random.nextInt(maxBound)%(maxBound-minBound+1)+minBound;
}

随机事件(指定事件发生概率)

大致的需求是:比如说,有三种情况,第一种情况的出现概率为2/5;第二种情况的出现概率为2/5;最后一种出现的概率为1/5。实现的原理比较简单,复用上面的方法,然后对随机的区域进行分割,判定最后随机数落在哪个范围内,返回所需对应的字符串,具体看代码注释,代码有点乱。之所以写这个主要是有次写测试的时候,请求接口会返回三种状态,所以写了一个,感觉比较有趣就记下来了。。

/**
 * events:key(事件):value(概率),用TreeMap保持加入的顺序
 * 0.4,0.4,0.2
 * 400,800,1000
 * @param events 随机事件
 * @return
 */
public static String probabilityEvent(TreeMap<String, Double> events){
    double p_total = 1.0;
    int random = getRandomNumInRange(0, 1000); //取出随机范围为0~999的一个随机数
    int index = 0;
    String result = "";
    for(Map.Entry<String, Double> event : events.entrySet()){
        p_total = p_total - event.getValue(); //用于检查事件总概率小于1
        if("".equals(event.getKey().trim()) || event.getValue()<=0.0 || event.getValue()>=1.0){
            throw new IllegalArgumentException("事件不得为空,事件概率不能小于0大于1");
        }
        int range = (int)(event.getValue()*1000); //计算出该概率在1000中所占的范围
        index = index + range;
        //用""来控制不重复获取事件
        if("".equals(result) && random<index){
            result = event.getKey();
        }
    }
    if(p_total<0.0){
        throw new IllegalArgumentException("事件概率相加必须小于1");
    }
    return result;
}

文件操作

ZIP压缩

下面这个是我根据网上代码来更改的,可以按照目录结构进行压缩,跳过空文件和隐藏文件,若已经有压缩文件存在,则直接返回。后面贴上具体效果。

如果按照正常的方法压缩在多线程下会出现各种问题:

问题描述1:在压缩文件的时候,一开始会创建一个zip压缩包。在压缩的过程这个压缩包不断被写入,字节数在增加,这时候另外一个线程来压缩文件,发现已经有这个压缩文件,删除。那之前的那个线程压缩的都白费了,已经被删了。

问题描述2:同上,只不过这次发现已经有这个压缩文件,直接返回,省的再压一次.。所以,同理,这次你会拿到一个无法访问的破损压缩文件。

比较两个问题描述,我觉得直接返回更好一点。

主要解决思路:

  1. 加锁,我想到两种方式,一种是synchronized或手动创建锁进行同步,另外一种是使用FileChannel中的加锁方法(我这里用了后者)
  2. 通过字节数是否有更新来判定文件是否被使用(网上看到的,有点啰嗦)
    /**
     * 对路径下的目录结构进行zip压缩(不压缩隐藏文件和空文件,其他格式可以通过
     * 后缀名自定义),适用于多线程下
     * @param srcDir 将压缩文件路径(文件夹),D:\\新建文件夹
     * @param zipPath 输出zip压缩包路径(不带压缩名),D:\\新建文件夹
     * @param zipName 压缩文件名 如,6666
     * @return true-压缩成功
     */
    public static boolean filesToZip(String srcDir, String zipPath, String zipName){
        ZipOutputStream zos = null;
        FileChannel fileChannel = null;
        FileLock fileLock = null;
        try {
            File zipFile = new File(zipPath + "/" + zipName + ".zip");
            //若已经有这个压缩包,直接返回
            //必须放在这里,因为打开输出流,会自动创建一个zip文件
            if(zipFile.exists()){
                return true;
            }
            FileOutputStream fos = new FileOutputStream(zipFile);
            //-------------------尝试获取锁------------------
            fileChannel = fos.getChannel();
            int trytimes = 0;
            while (trytimes<10) {
                try {
                    fileLock = fileChannel.tryLock();
                    break;
                } catch (Exception e) {
                    trytimes++;
                    Thread.sleep(1000);
                }
            }
            //----------------------------------------------
            zos = new ZipOutputStream(fos);
            File sourceFile = new File(srcDir);
            compress(sourceFile, zos, "");
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            try {
                if(fileLock != null){
                    fileLock.release();
                }
                if (zos != null) {
                    zos.close();
                }
                if(fileChannel != null){
                    fileChannel.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    public static Set<String> fileType = new HashSet<String>();
    private static String fileTypeStr = "zip"; //文件类型(后缀名逗号分开)
    static{
        String[] fileTypeArr = fileTypeStr.split(",");
        for(int i=0; i<fileTypeArr.length; i++){
            if(!StringUtils.isEmpty(fileTypeArr[i])){
                fileType.add(fileTypeArr[i]);
            }
        }
    }

    public static void compress(File sourceFile, ZipOutputStream zos, String name) throws Exception{
        byte[] buf = new byte[1024*6];
        if(sourceFile.isFile() && !sourceFile.isHidden()){
            //对文件类型进行控制
            String suffix = sourceFile.getName().substring(sourceFile.getName().lastIndexOf(".") + 1);
            if(fileType.contains(suffix.toLowerCase())){
                return;
            }
            if(name.indexOf("/") == 0){ //去掉前面多余的/
                name = name.substring("/".length(), name.length());
            }
            zos.putNextEntry(new ZipEntry(name));
            FileInputStream fis = new FileInputStream(sourceFile);
            BufferedInputStream bis = new BufferedInputStream(fis, 1024*6);
            int len;
            while((len = bis.read(buf, 0, 1024*6)) != -1){
                zos.write(buf, 0, len);
            }
            zos.closeEntry();
            bis.close();
            fis.close();
        }else{
            if(!sourceFile.isHidden()){
                File[] listFiles = sourceFile.listFiles();
                if(listFiles !=null && listFiles.length != 0){
                    for(int i=0 ;i<listFiles.length; i++){
                        compress(listFiles[i], zos, name + "/" + listFiles[i].getName());
                    }
                }
            }
        }
    }

压缩前

 压缩后

ZIP解压缩

    /**
     * 解压ZIP文件,并输出到指定目录中
     * @param zipFilePath zip文件的路径 D:\新建文件夹\6666.zip
     * @param unzipFilePath 输出的目录路径 D:\test
     * @return
     */
    public static boolean unZipFile(String zipFilePath, String unzipFilePath){
        if(!unzipFilePath.endsWith(File.separator)){
            unzipFilePath += File.separator;
        }
        try {
            ZipFile zipFile = new ZipFile(zipFilePath); //拿到zip文件对象
            ZipEntry zipEntry = null; //压缩包内的文件对象
            String entryName = null; //压缩包内的文件名
            Enumeration<? extends ZipEntry> enumeration = zipFile.entries(); //获得压缩包内所有对象
            int readBytes = 0;
            byte[] buf = new byte[1024*6];
            while(enumeration.hasMoreElements()){
                zipEntry = enumeration.nextElement();
                entryName = zipEntry.getName();
                if(zipEntry.isDirectory()){ //是否为文件夹
                    //mkdirs可建立多级文件夹/tmp/a/b/c/可以生成四个文件夹,而mkdir会因为没有找到返回false
                    new File(unzipFilePath + entryName).mkdirs();
                    continue;
                }
                //创建所在目录下的多级目录结构,打开输出流,生成文件
                File file = new File(unzipFilePath + entryName);
                file.getParentFile().mkdirs();
                OutputStream os = new FileOutputStream(file);
                InputStream is = zipFile.getInputStream(zipEntry);
                while((readBytes=is.read(buf)) != -1){
                    os.write(buf, 0, readBytes);
                }
                os.close();
                is.close();
            }
            zipFile.close();
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

配置文件读取

这里配置文件指的是properties文件,一般这类文件放在resources目录下

public class ServerProperties{
    private final Logger log = Logger.getLogger(ServerProperties.class);

    private static Properties prop = null;

    static{
        prop = new Properties();
        String filePath = ToolUtil.getClassPath(ServerProperties.class, "server.properties"); //自定义获取配置文件路径
        InputStream is = null;
        try{
            is = new BufferedInputStream(new FlieInputStream(filePath));
            prop.load(new InputStreamReader(is, "utf-8"));
        }catch(Exception e){
            log.error(e.getMessage, e);
        }
    }

    public static String getValue(){
        String result = "";
        if(prop != null){
            result = (String)prop.get(key);
        }
        return result;
    }
}

 自定义获取项目配置文件路径

    private static String configPath[] = {"/config/"};
    private static String defaultConfigPath = "/src/main/resources/";

    /**
     * 获取配置文件路径
     * @param clazz 配置类
     * @param fileName 需要读取的文件名
     * @return
     */
    public static String getClassPath(Class<?> clazz, String fileName){
        String userDir = System.getProperty("user.dir"); //项目路径
        String temp_path = null;
        File file = null;
        for(int i=0; i<configPath.length; i++){
            temp_path = userDir + configPath[i] + fileName;
            file = new File(temp_path);
            if(file.exists()){
                return temp_path;
            }
        }

        //在配置类所在的目录下
        //若是""则可以获得file:开头的绝对路径
        //若是"/"则可以获得target(IDEA),bin(eclipse)目录路径 --- classPath根目录
        URL url = clazz.getResource("");
        file = new File(url.getPath() + fileName);
        if(file.exists()){
            return url.getPath() + fileName
        }

        return userDir + defaultConfigPath + fileName;
    }

Java HTTP工具

将URL中的请求参数解析成Map

因为有遇到这么一个需求,就自己写了一个,记录一下。

/**
 * 测试示例:
 * type=test&sendtime=&
 * http://www.baidu.com?id=1&content=你好&sendTime=
 * map  ->->  type:test,sendtime:
 * 将url的请求参数解析到map中
 * @param url
 * @return
 */
public static Map<String, String> urlReqParamToMap(String url){
    HashMap<String, String> paramMap = new HashMap<String, String>();
    String paramStr = url.substring(url.indexOf("?") + 1); //若不存在?就取原串
    String[] args = paramStr.split("&");
    for(int i=0; i<args.length; i++){
        String[] argss = StringUtils.splitByWholeSeparator(args[i], "=");
        paramMap.put(argss[0], argss[1].trim());
    }
    return paramMap;
}

发送GET请求(http)

通过提取map中键值对来拼接URL请求参数:

/**
 * 根据键值对,创建HttpGet
 * @param httpUrl
 */
public static String sendHttpGet(String httpUrl, TreeMap<String, String> treeMap) {
    List<NameValuePair> params = new ArrayList<NameValuePair>();
    Iterator iterator = treeMap.entrySet().iterator();
    while(iterator.hasNext()){
        Map.Entry entry = (Map.Entry)iterator.next();
        params.add(new BasicNameValuePair(entry.getKey().toString(),            
                entry.getValue().toString()));
    }
    String str = "";
    try {
        str = EntityUtils.toString(new UrlEncodedFormEntity(params, Consts.UTF_8));
    } catch (IOException e) {
        e.printStackTrace();
    }
    HttpGet httpGet = new HttpGet(httpUrl + "?" + str);// 创建get请求
    return sendHttpGet(httpGet);
}

 创建HTTP客户端,发送GET请求:

private static CookieStore cookieStore = new BasicCookieStore();
private static RequestConfig requestConfig = RequestConfig.custom()
            .setSocketTimeout(15000)
            .setConnectTimeout(15000)
            .setConnectionRequestTimeout(15000)
            .setCookieSpec(CookieSpecs.STANDARD)
            .build();
private static PoolingHttpClientConnectionManager cm = null;

static{
    cm = new PoolingHttpClientConnectionManager();
    cm.setMaxTotal(200);
}

/**
 * 发送Get请求(创建get请求客户端)
 * @param httpGet
 * @return
 */
private static String sendHttpGet(HttpGet httpGet) {
        CloseableHttpClient httpClient = null;
        CloseableHttpResponse response = null;
        HttpEntity entity = null;
        String responseContent = null;
        try {
            // 创建默认的httpClient实例.
            httpClient = HttpClients.custom()
                    .setConnectionManager(cm)
                    .setDefaultRequestConfig(requestConfig)
                    .setDefaultCookieStore(cookieStore)
                    .setRedirectStrategy(new LaxRedirectStrategy())
                    .build();
            //给get请求也copy一份配置
            httpGet.setConfig(RequestConfig.copy(requestConfig).build());
            // 执行请求
            response = httpClient.execute(httpGet);
            entity = response.getEntity();
            //获取响应
            responseContent = EntityUtils.toString(entity, "UTF-8");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                // 关闭连接,释放资源
                if (response != null) {
                    response.close();
                }
                if (httpClient != null) {
                    httpClient.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return responseContent;
    }

发送Post请求(http)

    //请求设置
    private static CookieStore cookieStore = new BasicCookieStore();    
    private static RequestConfig requestConfig = RequestConfig.custom()
            .setSocketTimeout(15000)
            .setConnectTimeout(15000)
            .setConnectionRequestTimeout(15000)
            .setCookieSpec(CookieSpecs.STANDARD)
            .build();
    /**
     * 发送Post请求(创建post请求客户端)
     * @param httpPost
     * @return
     */
    private static String sendHttpPost(HttpPost httpPost) {
        CloseableHttpClient httpClient = null;
        CloseableHttpResponse response = null;
        HttpEntity entity = null;
        String responseContent = null;
        try {
            // 创建默认的httpClient实例.
            httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();
            httpPost.setConfig(requestConfig);
            // 执行请求
            response = httpClient.execute(httpPost);
            entity = response.getEntity();
            responseContent = EntityUtils.toString(entity, "UTF-8");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                // 关闭连接,释放资源
                if (response != null) {
                    response.close();
                }
                if (httpClient != null) {
                    httpClient.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return responseContent;
    }

    /**
     * 发送 post请求
     * @param httpUrl 地址
     * @param params 参数(格式:key1=value1&key2=value2)
     */
    public static String sendHttpPost(String httpUrl, String params) {
        HttpPost httpPost = new HttpPost(httpUrl);// 创建httpPost
        try {
            //设置参数
            StringEntity stringEntity = new StringEntity(params, "UTF-8");
            stringEntity.setContentType("application/x-www-form-urlencoded");
            httpPost.setEntity(stringEntity);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return sendHttpPost(httpPost);
    }

发送json数据的post请求

    public static String sendHttpPostJson(String httpUrl, String json) {
        HttpPost httpPost = new HttpPost(httpUrl);
        try {
            // 创建请求内容
            StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
            httpPost.setEntity(entity);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return sendHttpPost(httpPost);
    }

 发送https的Post和Get请求

和http不同的是,创建CloseableHttpClient的时候进行了一些设置

    private static SSLConnectionSocketFactory sslsf = null;
    private static PoolingHttpClientConnectionManager cm = null;
    private static SSLContextBuilder builder = null;
    static {
        try {
            builder = new SSLContextBuilder();
            // 全部信任 不做身份鉴定
            builder.loadTrustMaterial(null, new TrustStrategy() {
                @Override
                public boolean isTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
                    return true;
                }
            });
            sslsf = new SSLConnectionSocketFactory(builder.build(), new String[]{"SSLv2Hello", "SSLv3", "TLSv1", "TLSv1.2"}, null, NoopHostnameVerifier.INSTANCE);
            Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                    .register(HTTP, new PlainConnectionSocketFactory())
                    .register(HTTPS, sslsf)
                    .build();
            cm = new PoolingHttpClientConnectionManager(registry);
            cm.setMaxTotal(200);//max connection
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

//------------------

    httpClient = HttpClients.custom()
        .setDefaultCookieStore(cookieStore)
        .setRedirectStrategy(new LaxRedirectStrategy())
        .setSSLSocketFactory(sslsf)
        .setConnectionManager(cm)
        .setConnectionManagerShared(true)
        .build();

Java Redis

基础配置

根据redis实战来构建的环境,以后会根据需要更改。

首先要有一个redis的properties配置文件和spring-bean的配置文件。

redis.config.properties,放在resource下

###
dc_deal.ServerIp=127.0.0.1
dc_deal.ServerPort=6379
dc_deal.masterName=mymaster
dc_deal.maxTotal=3000
dc_deal.maxIdle=100
dc_deal.maxWaitMillis=3000
dc_deal.testOnBorrow=true

bean-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <context:property-placeholder location="redis.config.properties"/>
    <context:component-scan base-package="com.xck"/>

    <bean id="redisDeal" class="com.xck.adapter.impl.RedisDcAdapter" init-method="initPool">
        <property name="redisServerIp" value="${dc_deal.ServerIp}"/>
        <property name="redisServerPort" value="${dc_deal.ServerPort}"/>
        <property name="masterName" value="${dc_deal.masterName}"/>
        <property name="maxTotal" value="${dc_deal.maxTotal}"/>
        <property name="maxIdle" value="${dc_deal.maxIdle}"/>
        <property name="maxWaitMillis" value="${dc_deal.maxWaitMillis}"/>
        <property name="testOnBorrow" value="${dc_deal.testOnBorrow}"/>
    </bean>

</beans>

其次得有一个和spring-bean中对应的类,reids适配器RedisDcAdapter

package com.xck.adapter.impl;

import org.apache.log4j.Logger;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * redis适配器,用来初始化redis和获取redis连接池的工具
 */
public class RedisDcAdapter {
    protected Logger log = Logger.getLogger(RedisDcAdapter.class);
    private JedisPool pool = null;

    private String redisServerIp;
    private int redisServerPort;
    private String masterName;
    private int maxTotal;
    private int maxIdle;
    private int maxWaitMillis;
    private boolean testOnBorrow ;

    public synchronized void initPool(){
        jedisSinglePool();
    }

    public synchronized void jedisSinglePool(){
        pool = getSinglePool();
        log.info("initPool [Single] a pool hashCode:" + pool.hashCode() +
                ", redisServerIp: " + redisServerIp + ":" + redisServerPort);
    }

    public synchronized JedisPool getSinglePool(){
        if(pool == null){
            JedisPoolConfig config = new JedisPoolConfig();
            config.setMaxTotal(maxTotal);
            config.setMaxIdle(maxIdle);
            config.setMinIdle(maxIdle);
            config.setMaxWaitMillis(maxWaitMillis);
            config.setTestOnBorrow(testOnBorrow);
            pool = new JedisPool(config, redisServerIp, redisServerPort, maxWaitMillis);
        }
        return pool;
    }

    public Jedis getJedis(){
        return getSinglePool().getResource();
    }

    public void returnJedis(Jedis jedis){
        if(jedis != null){
            jedis.close();
        }
    }

    //一定要加set方法,因为spring配置文件使用的是property
    public void setRedisServerIp(String redisServerIp) {
        this.redisServerIp = redisServerIp;
    }

    public void setRedisServerPort(int redisServerPort) {
        this.redisServerPort = redisServerPort;
    }

    public void setMasterName(String masterName) {
        this.masterName = masterName;
    }

    public void setMaxTotal(int maxTotal) {
        this.maxTotal = maxTotal;
    }

    public void setMaxIdle(int maxIdle) {
        this.maxIdle = maxIdle;
    }

    public void setMaxWaitMillis(int maxWaitMillis) {
        this.maxWaitMillis = maxWaitMillis;
    }

    public void setTestOnBorrow(boolean testOnBorrow) {
        this.testOnBorrow = testOnBorrow;
    }
}

随后,是真正和reids交互的类,这里使用抽象类,因为如果有多个不同节点的处理端,这里可以直接继承。

公共类CommonRAO,主要定义了基本操作,使用抽象方法是为了根据需要获取不同的适配器。

package com.xck.rao;


import com.xck.adapter.impl.RedisDcAdapter;
import org.apache.log4j.Logger;
import redis.clients.jedis.Jedis;

/**
 * 通过redis适配器获取reids的连接,和redis进行交互
 */
public abstract class CommonRAO {
    protected Logger log = Logger.getLogger(CommonRAO.class);
    protected abstract RedisDcAdapter getDcAdapter();

    /**
     * 字符串操作
     * key-value
     * @param key
     * @param value
     */
    public void setString(String key, String value){
        Jedis jedis = null;
        try {
            jedis = getDcAdapter().getJedis();
            long start = System.currentTimeMillis();
            jedis.set(key, value);
            long end = System.currentTimeMillis();
            if(end - start>2000){
                log.warn("setString too long time keys:" + key + "; " + (end-start)+"ms");
            }
        } finally {
            getDcAdapter().returnJedis(jedis);
        }
    }
    public String getString(String key){
        Jedis jedis = null;
        String value = "";
        try {
            jedis = getDcAdapter().getJedis();
            long start = System.currentTimeMillis();
            value = jedis.get(key);
            long end = System.currentTimeMillis();
            if(end - start>2000){
                log.warn("getString too long time keys:" + key + "; " + (end-start)+"ms");
            }
        } finally {
            getDcAdapter().returnJedis(jedis);
        }
        return value;
    }

    /**
     * set集合(无序,不重复)元素添加(sadd),设置有效期(expire)
     * key-(member)
     * @param key
     * @param seconds 有效期,s
     * @param members
     * @return
     */
    public long addSet(String key, int seconds, String... members){
        Jedis jedis = null;
        long value = -1;
        try {
            jedis = getDcAdapter().getJedis();
            long start = System.currentTimeMillis();
            value = jedis.sadd(key, members);
            jedis.expire(key, seconds);
            long end = System.currentTimeMillis();
            if(end - start>2000){
                log.warn("addSet too long time keys:" + key + "; " + (end-start)+"ms");
            }
        } finally {
            getDcAdapter().returnJedis(jedis);
        }
        return value;
    }


    /**
     * 有序集合(无重复),元素添加
     * key-(member, score)
     * @param key
     * @param score
     * @param member
     * @return
     */
    public long addSortedSet(String key, double score, String member){
        Jedis jedis = null;
        long value = -1;
        try {
            jedis = getDcAdapter().getJedis();
            long start = System.currentTimeMillis();
            value = jedis.zadd(key, score, member);
            long end = System.currentTimeMillis();
            if(end - start>2000){
                log.warn("addSortedSet too long time keys:" + key + "; " + (end-start)+"ms");
            }
        } finally {
            getDcAdapter().returnJedis(jedis);
        }
        return value;
    }

    /**
     * 单个删除
     * @param key
     * @return
     */
    public long del(String key){
        Jedis jedis = null;
        long value = -1;
        try {
            jedis = getDcAdapter().getJedis();
            long start = System.currentTimeMillis();
            value = jedis.del(key);
            long end = System.currentTimeMillis();
            if(end - start>2000){
                log.warn("del too long time keys:" + key + "; " + (end-start)+"ms");
            }
        } finally {
            getDcAdapter().returnJedis(jedis);
        }
        return value;
    }
    /**
     * 批量删除
     * @param keys
     * @return
     */
    public long dels(String... keys){
        Jedis jedis = null;
        long value = -1;
        try {
            jedis = getDcAdapter().getJedis();
            long start = System.currentTimeMillis();
            value = jedis.del(keys);
            long end = System.currentTimeMillis();
            if(end - start>2000){
                log.warn("dels too long time keys:" + keys + "; " + (end-start)+"ms");
            }
        } finally {
            getDcAdapter().returnJedis(jedis);
        }
        return value;
    }
}

具体实现类DealRAO

package com.xck.rao;


import com.xck.adapter.impl.RedisDcAdapter;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;

@Repository
public class DealRAO extends CommonRAO{
    protected Logger log = Logger.getLogger(DealRAO.class);

    @Resource(name="redisDeal")
    protected RedisDcAdapter redisDcAdapter;

    @Override
    protected RedisDcAdapter getDcAdapter() {
        return redisDcAdapter;
    }
}

要使用的时候,直接用@Autowired自动注入即可。最后附上pom.xml配置

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>3.0.5.RELEASE</version>
        </dependency>

 利用redis实现输入自动补全

《Redis实战》中6.1节自动补全的java版本可以参考这篇文章:https://blog.youkuaiyun.com/woshixiazaizhe/article/details/80451198

自动补全的场景随处可见:浏览器网址输入,搜索框,查找框的输入。

我感觉这么做的优点是:

  1. 在内存中匹配速度快;
  2. 减少和关系型数据库的交互;
  3. 将匹配的工作交给redis,而不是将匹配工作放到外面,可以减少资源的浪费。因为若是将匹配工作放到外面,则一次要获取全部元素进行匹配工作,在数量巨大的情况下,这样做就不是很明智了。

原理:

  1. 你需要找出自己想要支持的字符范围,并确保该范围前后都要有一个字符;
  2. 使用Redis的有序集合来存储数据,将分值全置为0,这样有序集合发现分值都相同了就会根据成员的二进制顺序进行排序;
  3. 给定一个需要补全的前缀字符串,拿到该前缀字符串的最后一个字符,找出该字符的前驱字符,和所支持的字符范围内最大字符的后面一个字符进行拼接,生成前缀字符串所能表示的最大范围的两个字符串,插入有序集合中。
  4. 取出这两个字符串的索引,并获取该范围内的字符串,同时删除之前插入的两个字符串。

有点绕,举个例子就很明白了:

所支持字符范围:a~z。

该范围前后字符分别为:`和{,(反引号和左花括号)。

前缀字符串:"comm"。

m的前面是l,后面是n,所以生成的标识范围的起始字符串为"coml{"和结束字符串"comn{"。

将这两个字符串插入有序集合中就会自动找出一个范围,”coml{“<”comm...“<"comn{",该范围全都是以comm开头的字符串。


Java数据库


Java Spring

Spring定时功能的注解实现

定时任务的需求很常见,网上有很多资料,但是问题也是相同的,那就是如果在系统启动后修改系统时间,你会发现任务失效了,不会执行;而在修改之前就不会

未完待续。。。。


Java数据结构

栈-多级md5加密

写的玩的,主要是复习一下栈的四则运算应用。

写的目的,是为了多级md5加密,通过解析json字符串来加密多个变量名。

例如:md5(md5(a)+b+md5(c+d))和md5(md5(a+b)+md5(c+d)+e+f)

 主要思路:将表达式转换为后缀表达式后放入队列中,然后使用栈对其进行加密操作,因为模仿的是四则运算,所以需要两个操作数,对于单个变量名加密,使用#代替另外一个操作数;而操作数使用的是变量名,所以考虑使用map来保存变量名和值的关系,到时加密的时候直接进行变量名替换,进行替换的位置可以在提取变量名或者md5加密的时候。

    /**
     * 该方法用来多级md5加密的,定义+为字符串拼接,-为md5加密
     * #是用来适配单个变量名md5加密的比如:md5(d) = d-#或#-d
     * #-((#-a)+b+(c-d)) = md5(md5(a)+b+md5(c+d))
     * ((a-b)+(c-d)+e+f)-# = md5(md5(a+b)+md5(c+d)+e+f)
     * @param md5Str
     * @param operatorStr 利用字符串来减少判断
     * @return
     */
    public static String multiMD5Str(String md5Str, String operatorStr){
        Stack<String> operatorCharacter = new Stack<String>(); //操作符栈
        Queue<String> postFixExpression = new ArrayQueue<String>(); //后缀表达式队列

        for(int i=0; i<md5Str.length(); i++){
            switch (md5Str.charAt(i)){
                case '(':
                case '+':
                case '-':
                    //+,-,(,都是直接压栈
                    operatorCharacter.push(md5Str.charAt(i) + "");
                    break;
                case ')':
                    //若是右括号则弹出操作符直到遇到左括号,指向下一个
                    while(!operatorCharacter.peek().equals("(")){
                        postFixExpression.offer(operatorCharacter.pop());
                    }
                    operatorCharacter.pop(); //将左括号出栈
                    break;
                default:
                    //若不是符号,则将其拼接成变量名放入字符串数组中
                    StringBuilder sf = new StringBuilder();
                    while(i<md5Str.length() && !operatorStr.contains(md5Str.charAt(i)+"")){
                        sf.append(md5Str.charAt(i));
                        i++;
                    }
                    postFixExpression.offer(sf.toString());
                    i--;
            }
        }
        //将操作符栈中符号全部入队,[a, b, -, c, d, -, e, f, +, +, +, -]
        while(!operatorCharacter.isEmpty()){
            postFixExpression.offer(operatorCharacter.pop());
        }

        operatorCharacter.clear(); //复用栈,用于计算表达式
        while(!postFixExpression.isEmpty()){
            //若遇到操作数直接压栈
            if(!operatorStr.contains(postFixExpression.peek())){
                operatorCharacter.push(postFixExpression.poll());
                continue;
            }

            if(postFixExpression.isEmpty()){
                break;
            }
            //若遇到操作符则出栈两个元素和一个操作符
            String a = operatorCharacter.pop();
            String b = operatorCharacter.pop();
            String operator = postFixExpression.poll();
            //可以用一个map来存储变量名,这里就可以直接取值替换,或者在上面提取变量名的时候替换
            if("-".equals(operator)){
                a = a.equals("#")?"":a;
                b = b.equals("#")?"":b;
                operatorCharacter.push(md5Convert(b+a));
            }else if("+".equals(operator)){
                operatorCharacter.push(b+a);
            }
        }

        return operatorCharacter.pop();
    }

 

中缀转后缀:(从左到右遍历中缀表达式)

  1. 准备一个栈和一个队列,一个用于存储操作符,另一个存储后缀表达式;
  2. 遇到'(','操作符'全部压栈;
  3. 遇到')',不断出栈,直到遇到'(',将'('也出栈,出栈的元素全部入队;
  4. 遇到其余均当做变量名处理,拼接变量名,入队;
  5. 将栈剩余元素出栈,全部入队;

四则运算表达式求值:(遍历队列,不断出队)

  1. 准备一个栈;
  2. 若遇到出队的是操作数,直接压栈;
  3. 若遇到出队的是'操作符',出栈两个操作数,根据操作符进行表达式计算,然后将结果压栈;
  4. 最后队列空了表示计算完成,栈中只剩下一个元素就是最后结果,直接出栈;

参考的网址:

  1. 中缀表达式转换为后缀表达式
  2. 四则运算表达式求值

单调队列应用于区间求和

我也是昨天才开始研究这部分,先记下我目前的理解以免忘记。

参考文章:

  1. 单调队列的一些应用:https://blog.youkuaiyun.com/er111er/article/details/78344161
  2. 单调队列的介绍:https://www.cnblogs.com/tham/p/8038828.html

单调队列是什么:顾名思义就是单调的队列,队列中元素的排列是单调递增或递减的。

单调递增队列的进出方式在第一篇参考文章中有一个很形象的描述:

在食堂中排队打饭,打完饭的人从队头出队,后面来的从队尾入队排队打饭。这时有一个人壮汉走过来看见这么多人排队,心中烦躁不已,就从队尾开始赶人(不是插队而是赶跑),直到他打不过为止。和普通队列不同,打饭的人既可以打完饭从队头出去,也可以被人从后面(队尾)赶出去,而进入队列的唯一途径就是从队尾进入,属于双端队列(两端出队,一端入队)

在上面的场景中,单调队列是以战斗力来排的,越靠队头,战斗力越强。

单调递增队列进出方式模拟:

    /**
     * 单调递减队列模拟
     * @param numbers 队列
     * @param queueLen 队列长度
     */
    public static void monotoneDecQueue(int[] numbers, int queueLen){
        //双端队列
        ArrayDeque<Integer> queue = new ArrayDeque<Integer>(queueLen);

        for(int i=0; i<numbers.length; i++){
            int temp = numbers[i]; //获取目标元素
            if(queue.isEmpty()){ //队列为空直接入队
                queue.offer(temp);
                System.out.println(i + " " + queue); //输出第i+1步,队列情况
                continue;
            }
            //若队尾元素不大于目标元素,不符合单调性,从队尾出队
            while(!queue.isEmpty() && queue.peekLast() <= temp){
                queue.pollLast();
            }
            //若队列已满,而且队尾又比目标元素大,符合单调性,从队头出队
            while(queue.size() == queueLen && queue.peek() >= temp){
                queue.poll();
            }
            queue.offer(temp); //无论如何目标元素都会入队
            System.out.println(i + " " + queue);
        }
    }

测试结果:

monotoneDecQueue(new int[]{6,4,10,10,8,6,4,2,12,14}, 3);
0 [6]
1 [6, 4]
2 [10]
3 [10]
4 [10, 8]
5 [10, 8, 6]
6 [8, 6, 4]
7 [6, 4, 2]
8 [12]
9 [14]

单调递增队列反过来即可。

先来看一道题目,有兴趣可以看参考文章第二篇里面的讲解。

题目大概意思是:某公司的员工之前每天加班加点的赶项目,现在项目圆满完工,公司准备给员工放假。假期可以在1~N天之中任选一段连续的天数,而且每天都有一定的享受值。为了让员工充分的放松,享受生活,特意限定假期的天数要在p天~q天之间(也就是p<=假期天数<=q)。那么应该要如何选择才能最享受呢?(感觉着题目就搞笑)

分析题目我们可以知道,已知可以选的天数是1~N,每天享受值为Xn,并且所选的天数区间必须在[p,q]之间。

要问的其实是两个问题:享受值最大是多少?是哪天到哪天?

假设:一共5天,每天享受值为-9 -4  -3  8  -6,假期天数必须在[2,4]之间。

第一种解法:

求某个区间的和最大,我的第一个想法就是一个个遍历过去。对于每个数字都必须往后取一天(因为不能小于2天),然后再往后取1到2天,当后面取的1到2天的享受值加到之前选定的两天的值中变小了则放弃此次求和,往后移动,从下一个重新开始。

以上面的假设为例子:

i=1,享受值=-13(第一天和第二天,两天相加),往后取一天发现享受值<0,i++,此次享受值最大为:-13,记录位置;

i=2,享受值=-7,往后取一天>0,享受值为1,再往后取一天<0,i++,最值为1,记录位置;

i=3,享受值5,往后取一天<0,放弃,最值为5,记录位置;

i=4,。。。以此类推,最后结果是3~4天,最值为5。

第二种解法:

就是参考文章中所述的解法了,利用了单调递增队列的特性。

队列长度为4,位于队头的元素是前缀和最小的,单调递增。而队头元素存放的是区间[i,j]中i-1的前缀和。啥意思呢?

前缀和:就是当前位置上的元素代表的是你前面所有元素的和(包括你自己)。

比如上面例子中享受值的前缀和就是:-9 -13  -16  -8  -14;

前缀和的计算公式:sum[i] = sum[i-1] + a[i](递归公式,sum-前缀和数组,a-元素数组)。

那么为什么要操作前缀和呢?

给[i,j]区间求和的公式:[i,j]之和 = sum[j]-sum[i-1],在 sum[j]固定的情况下,sum[i-1]越小,[i,j]区间和就越大。

这里因为假设sum[j]是固定的,而sum[i-1]是可变的,相当于固定一点往前取天数,和第一种解法恰恰相反;而又因为取天数最少要取两天(往前取一天是为了满足题意必须两天以上,再取一天是为了求区间和),所以遍历的时候从第3天开始

开始计算:

i=3,sum[i-1]->sum[(3-1)-1]=-9(这里3-1和i是等价的),sum[j]->sum[i]=-16;队列为空,直接入队,队列[-9];求区间和,取最大值-7;

i=4,sum[i-1]->sum[(4-1)-1]=-13,sum[j]->sum[i]=-8;取队尾元素-9,发现比-13还大,-9出队,-13入队,队列[-13];求区间和,取最大值5(sum[j]-sum[i-1]=-8+13=5>-7);

i=5,sum[i-1]->sum[(5-1)-1]=-16,sum[j]->sum[i]=-14,取队尾元素-13,发现比-16小,-13出队,-16入队,队列[-16];求区间和,取最值还是5;

可以看出队列中操作的元素是sum[i-1],每次取最大值的时候都取队头元素通过sum[j]-sum[i-1]公式计算区间和,在和上一次的区间和比较。

    /**
     * 区间求最大值
     * range=4
     * numbers=-9 -13  -16  -8  -14 || -9,-4,-3,8,-6
     * [1,k] = sum[k] --> [x,y] = sum[y]-sum[x-1] 和越大sum[x-1越小]
     * @param numbers 前缀数组
     * @param minRange 区间长度最小值
     * @param maxRange 区间长度最大值
     */
    public static void getRangeMin(int[] numbers, int minRange, int maxRange){
        //双端队列
        ArrayDeque<Integer> queue = new ArrayDeque<Integer>(maxRange);
        int min = -2147483647;

        for(int i=minRange; i<numbers.length; i++){
            //[x,y] = sum[y]-sum[x-1] --> [i-minRange+1, i] = sum[i]-sum[i-minRange]
            //这里取sum[i-minRange],将当前天数往前数minRange-1天+1
            int temp = numbers[i-minRange];
            if(queue.isEmpty()){ //队列为空,直接入队
                queue.offer(temp);
                min = Math.max(min, numbers[i]-queue.peek());
                System.out.println(queue + " " + (i+1));
                System.out.println(min);
                continue;
            }
            //取最小,和之前的sum[i-minRange],(sum[x-1])相比,哪个小
            while(!queue.isEmpty() && queue.peekLast() >= temp){
                queue.pollLast();
            }
            //若队列满了,若队尾不比temp大,(sum[x-1])则入队,去掉队头
            while(queue.size() == maxRange && queue.peek() <= temp){
                queue.poll();
            }
            //入队,若队尾比队头小,而且队列又没满,入队
            queue.offer(temp);
            //numbers[i]-queue.peek() --> sum[y]-sum[x-1]
            min = Math.max(min, numbers[i]-queue.peek());
            System.out.println(queue + " " + (i+1));
            System.out.println(min);
        }
    }

    /**
     * 求int数组的前缀和
     * [i,k]=sum[k]
     * sum[i] = sum[i-1] + a[i]
     * -9,-4,-3,8,-6 -> -9 -13 -16 -8 -14
     * @param numbers
     * @return
     */
    public static int[] getSuffixSum(int[] numbers){
        int[] sum = numbers.clone();
        for(int i=0 ;i+1<numbers.length; i++){
            sum[i+1] = sum[i]+numbers[i+1];
        }
        return sum;
    }

 测试结果:

int[] numbers = getSuffixSum(new int[]{-9,-4,-3,8,-6});
getRangeMin(numbers, 2, 4);

[-9] 3
-7
[-13] 4
5
[-16] 5
5

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值