android 字符串工具类(兼容 Android 16+ / API 16,无报错版)

复制代码
package com.nyw.mvvmmode.utils;


import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Pattern;

/**
 * 字符串工具类(兼容 Android 16+ / API 16,无报错版)
 * 功能:空值判断、格式校验、类型转换、日期处理、字符串加工、字节数组转换
 */
public final class StringUtils {
    // ========================= 正则常量(修复手机号正则,适配全号段)=========================
    private static final Pattern PATTERN_EMAIL = Pattern.compile("\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*");
    private static final Pattern PATTERN_PHONE = Pattern.compile("^1[3-9]\\d{9}$"); // 支持13-19全号段
    private static final Pattern PATTERN_PURE_NUMBER = Pattern.compile("^\\d+$");
    private static final Pattern PATTERN_CHINESE = Pattern.compile("[\u4e00-\u9fa5]");
    private static final String[][] HTML_ESCAPE_PAIRS = {
            {"&", "&amp;"}, {"<", "&lt;"}, {">", "&gt;"},
            {"\"", "&quot;"}, {"'", "'"}
    };

    // ========================= 日期格式化(ThreadLocal保证线程安全,补充Locale)=========================
    private static final ThreadLocal<SimpleDateFormat> SDF_FULL = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
        }
    };

    private static final ThreadLocal<SimpleDateFormat> SDF_DATE = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
        }
    };

    // ========================= 私有构造(防止实例化)=========================
    private StringUtils() {
        throw new AssertionError("StringUtils cannot be instantiated");
    }

    // ========================= 1. 空值判断(修复逻辑效率,避免冗余判断)=========================
    public static boolean isEmpty(CharSequence input) {
        if (input == null || input.length() == 0) {
            return true;
        }
        for (int i = 0; i < input.length(); i++) {
            if (!Character.isWhitespace(input.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    public static boolean hasEmpty(CharSequence... strs) {
        if (strs == null || strs.length == 0) {
            return true;
        }
        for (CharSequence str : strs) {
            if (isEmpty(str)) {
                return true;
            }
        }
        return false;
    }

    public static String emptyReplace(CharSequence input, String defaultValue) {
        return isEmpty(input) ? defaultValue : input.toString();
    }

    // ========================= 2. 格式校验(修复原正则缺陷,补充容错)=========================
    public static boolean isEmail(CharSequence email) {
        return !isEmpty(email) && PATTERN_EMAIL.matcher(email).matches();
    }

    public static boolean isPhone(CharSequence phoneNum) {
        return !isEmpty(phoneNum) && PATTERN_PHONE.matcher(phoneNum).matches();
    }

    public static boolean isPureNumber(CharSequence str) {
        return !isEmpty(str) && PATTERN_PURE_NUMBER.matcher(str).matches();
    }

    public static boolean containsChinese(CharSequence str) {
        return !isEmpty(str) && PATTERN_CHINESE.matcher(str).find();
    }

    // ========================= 3. 类型转换(修复空指针风险,明确异常捕获)=========================
    public static int toInt(CharSequence str, int defValue) {
        if (isEmpty(str)) {
            return defValue;
        }
        try {
            return Integer.parseInt(str.toString());
        } catch (NumberFormatException e) {
            return defValue;
        }
    }

    public static int toInt(Object obj) {
        if (obj == null) {
            return 0;
        }
        return toInt(obj.toString(), 0);
    }

    public static long toLong(CharSequence str) {
        if (isEmpty(str)) {
            return 0L;
        }
        try {
            return Long.parseLong(str.toString());
        } catch (NumberFormatException e) {
            return 0L;
        }
    }

    public static double toDouble(CharSequence str) {
        if (isEmpty(str)) {
            return 0.0D;
        }
        try {
            return Double.parseDouble(str.toString());
        } catch (NumberFormatException e) {
            return 0.0D;
        }
    }

    public static boolean toBool(CharSequence str) {
        return !isEmpty(str) && Boolean.parseBoolean(str.toString().toLowerCase(Locale.getDefault()));
    }

    // ========================= 4. 日期处理(修复时区判断逻辑,避免空指针)=========================
    public static String getCurrentTime(String format) {
        if (isEmpty(format)) {
            return "";
        }
        try {
            SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.getDefault());
            return sdf.format(new Date());
        } catch (IllegalArgumentException e) {
            return ""; // 格式非法时返回空
        }
    }

    public static Date toDate(String sdate) {
        return toDate(sdate, SDF_FULL.get());
    }

    public static Date toDate(String sdate, SimpleDateFormat sdf) {
        if (isEmpty(sdate) || sdf == null) {
            return null;
        }
        try {
            return sdf.parse(sdate);
        } catch (ParseException e) {
            return null;
        }
    }

    public static String friendlyTime(String sdate) {
        Date time = toDate(sdate);
        if (time == null) {
            return "未知时间";
        }
        // 处理时区(修复原代码冗余判断)
        if (!isInEasternEightZones()) {
            time = transformTime(time, TimeZone.getTimeZone("GMT+08"), TimeZone.getDefault());
        }

        Calendar cal = Calendar.getInstance();
        long currentTime = cal.getTimeInMillis();
        long targetTime = time.getTime();
        String curDate = SDF_DATE.get().format(cal.getTime());
        String targetDate = SDF_DATE.get().format(time);

        // 当天时间处理
        if (curDate.equals(targetDate)) {
            long hourDiff = (currentTime - targetTime) / 3600000L;
            if (hourDiff == 0) {
                long minuteDiff = (currentTime - targetTime) / 60000L;
                return Math.max(minuteDiff, 1) + "分钟前";
            } else {
                return hourDiff + "小时前";
            }
        }

        // 非当天时间处理
        long dayDiff = (currentTime / 86400000L) - (targetTime / 86400000L);
        if (dayDiff == 1) {
            return "昨天";
        } else if (dayDiff == 2) {
            return "前天";
        } else if (dayDiff > 2 && dayDiff < 31) {
            return dayDiff + "天前";
        } else if (dayDiff >= 31 && dayDiff <= 62) {
            return "1个月前";
        } else if (dayDiff > 62 && dayDiff <= 93) {
            return "2个月前";
        } else if (dayDiff > 93 && dayDiff <= 124) {
            return "3个月前";
        } else {
            return targetDate;
        }
    }

    public static boolean isInEasternEightZones() {
        // 修复原代码equals判断问题(通过时区ID比较,更可靠)
        String timeZoneId = TimeZone.getDefault().getID();
        return "GMT+08".equals(timeZoneId) || "Asia/Shanghai".equals(timeZoneId);
    }

    public static Date transformTime(Date date, TimeZone oldZone, TimeZone newZone) {
        if (date == null || oldZone == null || newZone == null) {
            return null;
        }
        int offset = oldZone.getOffset(date.getTime()) - newZone.getOffset(date.getTime());
        return new Date(date.getTime() - offset);
    }

    // ========================= 5. 字符串加工(新增实用功能,无高版本API)=========================
    public static String desensitizePhone(CharSequence phoneNum) {
        if (!isPhone(phoneNum)) {
            return emptyReplace(phoneNum, "");
        }
        String phone = phoneNum.toString();
        return phone.substring(0, 3) + "****" + phone.substring(7);
    }

    public static String substringWithEllipsis(CharSequence str, int maxLength) {
        if (isEmpty(str) || maxLength <= 0) {
            return emptyReplace(str, "");
        }
        String s = str.toString();
        return s.length() <= maxLength ? s : s.substring(0, maxLength) + "...";
    }

    public static String escapeHtml(CharSequence html) {
        if (isEmpty(html)) {
            return "";
        }
        String result = html.toString();
        for (String[] pair : HTML_ESCAPE_PAIRS) {
            result = result.replace(pair[0], pair[1]);
        }
        return result;
    }

    // ========================= 6. 字节数组与十六进制(保留原功能,修复语法错误)=========================
    public static String byteArrayToHexString(byte[] data) {
        if (data == null || data.length == 0) {
            return "";
        }
        StringBuilder sb = new StringBuilder(data.length * 2);
        for (byte b : data) {
            int v = b & 0xFF; // 修复原代码强转问题,确保无符号
            if (v < 16) {
                sb.append('0');
            }
            sb.append(Integer.toHexString(v));
        }
        return sb.toString().toUpperCase(Locale.getDefault());
    }

    public static byte[] hexStringToByteArray(String s) {
        if (isEmpty(s) || s.length() % 2 != 0) {
            return new byte[0]; // 长度为奇数时返回空数组,避免崩溃
        }
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            // 修复原代码Character.digit可能返回-1的问题,补充容错
            int high = Character.digit(s.charAt(i), 16);
            int low = Character.digit(s.charAt(i + 1), 16);
            if (high == -1 || low == -1) {
                return new byte[0]; // 非法十六进制字符时返回空数组
            }
            data[i / 2] = (byte) ((high << 4) + low);
        }
        return data;
    }
}

验证与使用建议

  1. 兼容性验证 :所有 API 均基于 Android 16(API 16)及以下已支持的类 / 方法(如 ThreadLocalSimpleDateFormatPattern 等),无高版本依赖,可直接在低版本设备上运行。

  2. 调用示例

    java

    运行

    复制代码
    // 1. 手机号脱敏
    String desensitized = StringUtils.desensitizePhone("13800138000"); // 结果:138****8000
    
    // 2. 友好时间显示
    String friendly = StringUtils.friendlyTime("2024-05-20 10:30:00"); // 结果:如"1小时前"
    
    // 3. 字节数组转十六进制
    byte[] bytes = "test".getBytes();
    String hex = StringUtils.byteArrayToHexString(bytes); // 结果:54657374
相关推荐
友人.2273 分钟前
Android 底部导航栏 (BottomNavigationView) 制作教程
android
努力学习的小廉30 分钟前
初识MYSQL —— 事务
android·mysql·adb
阿里云云原生1 小时前
深度解析 Android 崩溃捕获原理及从崩溃到归因的闭环实践
android
.豆鲨包1 小时前
【Android】Android内存缓存LruCache与DiskLruCache的使用及实现原理
android·java·缓存
JulyYu2 小时前
【Android】针对非SDK接口的限制解决方案
android·客户端
猪哥帅过吴彦祖2 小时前
Flutter 系列教程:应用导航 - Navigator 1.0 与命名路由
android·flutter·ios
2501_916008893 小时前
iOS 跨平台开发实战指南,从框架选择到开心上架(Appuploader)跨系统免 Mac 发布全流程解析
android·macos·ios·小程序·uni-app·iphone·webview
stevenzqzq4 小时前
Android Hilt教程_构造函数
android
鹏多多4 小时前
flutter图片选择库multi_image_picker_plus和image_picker的对比和使用解析
android·flutter·ios