【Java SE语法篇】10.String类

微信图片_20231214093055

📚博客主页:爱敲代码的小杨.

✨专栏:《Java SE语法》

❤️感谢大家点赞👍🏻收藏⭐评论✍🏻,您的三连就是我持续更新的动力❤️

文章目录

  • 前言
  • 1. String类
    • 1.1 字符串的构造
    • 1.2 String对象的比较
      • 1. ==比较是否引用同一个对象
      • 2. equals()方法:按照字典序比较
      • 3. compareTo()方法: 按照字典序进行比较
      • 4. 忽略大小写比较
    • 1.3 字符串查找
    • 1.4 转换
      • 1. 数值和字符串转化
      • 2. 大小写转化
      • 3. 字符串转数组
      • 4. 格式化
    • 1.5 字符串替换
    • 1.6 字符串拆分
    • 1.7 字符串的截取
    • 1.8 其他操作
  • 2. StringBuilde 类 和 StringBuffer类

前言

在程序开发中经常会用到字符串。字符串是指一连串的字符,它是由许多单个字符连接而成的,如多个英文字母所组成的英文单词。字符串可以包含任意字符,这些字符必须包含在一对双引号""之内,例如:“abc”。Java定义了3个封装字符串的类,分别是String类、StringBuffer类和StringBulider类。它们位于java.lang 包中,并提供了一系列操作字符串的方法,这些方法不需要导包就可以直接使用。下面将对String类、StringBuffer类和StringBulider类进行讲解。

1. String类

1.1 字符串的构造

String类提供了构造方法非常多,常用的就以下三种:

public class Main {public static void main(String[] args) {// 使用常量串构造String s1 = "hello";System.out.println(s1);// 直接new String对象String s2 = new String("hello");System.out.println(s2);// 使用字符数组进行构造char[] chars = {'h','e','l','l','o'};String s3 = new String(chars);System.out.println(s3);}
}

其他方法需要用到时,大家参考Java在线文档:String官方文档

【注意】:

  1. String是引用类型,内部并不存储字符串本身,在String类的实现源码中,String类实例变量如下: 在这里插入图片描述

    public class Main {public static void main(String[] args) {// s1和s2引用的是不同对象  s1和s3引用的是同一对象String s1 = new String("hello");String s2 = new String("world");String s3 = s1; // s3这个引用指向了s1这个引用的对象System.out.println(s3); // helloSystem.out.println(s1.length());// 获取字符串的长度System.out.println(s1.isEmpty());// 如果字符串长度为0,返回true,否则返回falseString s4 = "";System.out.println(s4.length()); // 0System.out.println(s4.isEmpty());// true
    }
    

    内存图:

    image-20240106184237523


    image-20240106184609235

  2. 在Java中""引起来的也是String类型对象

    // 打印"hello"字符串(String对象)的长度
    System.out.println("hello".length());// 5
    

1.2 String对象的比较

字符串的比较是常见操作之一,比如:字符串排序。Java中总共提供了4种方式:

1. ==比较是否引用同一个对象

注意:对于内置类型,==比较的是变量中的值;对于引用类型==比较的是引用中的地址。

public class Main {public static void main(String[] args) {int a = 10;int b = 20;int c = 10;// 对于基本类型变量,==比较两个变量中存储的值是否相同System.out.println(a == b);    // falseSystem.out.println(a == c);    // true// 对于引用类型变量,==比较两个引用变量引用的是否为同一个对象String s1 = new String("hello");String s2 = new String("hello");String s3 = new String("world");String s4 = s1;System.out.println(s1 == s2);   // falseSystem.out.println(s2 == s3);   // falseSystem.out.println(s1 == s4);   // true}
}

2. equals()方法:按照字典序比较

字典序:字符大小的顺序

String类重写了父类Objectequals方法,Objectequals默认按照==比较,String重写equals方法后,按照 如下规则进行比较,比如:s1.equals(s2)

image-20240106200110124

public class Main {public static void main(String[] args) {String s1 = new String("hello");String s2 = new String("hello");String s3 = new String("Hello");// s1、s2、s3引用的是三个不同对象,因此==比较结果全部为falseSystem.out.println(s1 == s2);       // falseSystem.out.println(s1 == s3);       // false// equals比较:String对象中的逐个字符// 虽然s1与s2引用的不是同一个对象,但是两个对象中放置的内容相同,因此输出true// s1与s3引用的不是同一个对象,而且两个对象中内容也不同,因此输出falseSystem.out.println(s1.equals(s2));  // trueSystem.out.println(s1.equals(s3));  // false}
}

【注意】:为什么以下代码输出的结果都是true

image-20240106200646853

答:因为在 Java 中有一块特殊的内存(常量池),存储在堆上。

它的作用是什么呢?

  1. 只要是""双引号括起来的字符串存放在这里。
  2. 存储字符串之前它会找常量池里是否存在这个字符串,如果有就不存放了(常量池不会重复存放相同的值),所以上述代码中s1s2都指向常量池hello的地址。

image-20240106202614366

3. compareTo()方法: 按照字典序进行比较

equals不同的是,equals返回的是boolean类型,而compareTo返回的是int类型。具体比较方式:

  1. 先按照字典次序大小比较,如果出现不等的字符,直接返回这两个字符的大小差值
  2. 如果前k个字符相等(k为两个字符长度最小值),返回值两个字符串长度差值
public class Main {public static void main(String[] args) {String s1 = new String("abc");String s2 = new String("ac");String s3 = new String("abc");String s4 = new String("abcdef");// s1 和 s2 比较大小 s1 > s2 返回大于0的数字 s1 < s2 返回小于0的数字 否则返回0// 返回差值就是对应acsii码的差值System.out.println(s1.compareTo(s2));   // 不同输出字符差值-1System.out.println(s1.compareTo(s3));   // 相同输出 0System.out.println(s1.compareTo(s4));   // 前k个字符完全相同,输出长度差值 -3}
}

4. 忽略大小写比较

  • equalsIgnoreCase()方法:与equals()方式相同,但是忽略大小写比较。

  • compareToIgnoreCase()方法:与compareTo()方式相同,但是忽略大小写比较。

public class Main {public static void main(String[] args) {String s1 = new String("abc");String s2 = new String("Abc");System.out.println(s1.equals(s2)); // falseSystem.out.println(s1.equalsIgnoreCase(s2)); // trueSystem.out.println(s1.compareTo(s2));//32System.out.println(s1.compareToIgnoreCase(s2));// 0}
}

1.3 字符串查找

字符串查找也是字符串中非常常见的操作,String类提供的常用查找的方法

方法功能
char charAt(int index)返回index位置上字符,如果index为负数或者越界,抛出 IndexOutOfBoundsException异常
int indexOf(int ch)返回ch第一次出现的位置,没有返回-1
int indexOf(int ch, int fromIndex)从fromIndex位置开始找ch第一次出现的位置,没有返回-1
int indexOf(String str)返回str第一次出现的位置,没有返回-1
int indexOf(String str, int fromIndex)从fromIndex位置开始找str第一次出现的位置,没有返回-1
int lastIndexOf(int ch)从后往前找,返回ch第一次出现的位置,没有返回-1
int lastIndexOf(int ch, int fromIndex)从fromIndex位置开始找,从后往前找ch第一次出现的位置,没有返 回-1
int lastIndexOf(String str)从后往前找,返回str第一次出现的位置,没有返回-1
int lastIndexOf(String str, int fromIndex)从fromIndex位置开始找,从后往前找str第一次出现的位置,没有返 回-1
public class Main {public static void main(String[] args) {String s1 = new String("hello");// 返回字符串对应下标的字符System.out.println(s1.charAt(1)); // e//返回对应字符出来的下标位置 从头开始查找System.out.println(s1.indexOf('e')); // 1//返回对应字符出来的下标位置 从指定位置查找System.out.println(s1.indexOf('l', 3)); // 3// 字符串查找 从一个字符串找另一个字符串System.out.println(s1.indexOf("llo")); // 2System.out.println(s1.indexOf("ll", 2));// 2// 返回对应字符出来的下标位置 从尾开始向前查找System.out.println(s1.lastIndexOf('l'));// 3// 返回对应字符出来的下标位置 从指定位置向前查找System.out.println(s1.lastIndexOf('l', 1));// -1System.out.println(s1.lastIndexOf("ll")); // 2System.out.println(s1.indexOf("ll", 1));// 2}
}

1.4 转换

1. 数值和字符串转化

public class Main {public static void main(String[] args) {// 数字转字符串String s1 = String.valueOf(123);System.out.println(s1);String s2 = String.valueOf(12.34);System.out.println(s2);String s3 = String.valueOf(true);System.out.println(s3);// 字符串转数字int num1 = Integer.parseInt("1234");System.out.println(num1);double num2 = Double.parseDouble("12.34");System.out.println(num2);}
}

2. 大小写转化

public class Main {public static void main(String[] args) {// 小写转大写String s1 = "hello";System.out.println(s1.toUpperCase());// 大写转小写String s2 = "HELLO";System.out.println(s2.toLowerCase());}
}

问题:转化为大写/小写是在原来的字符串上进行修改的?

答:不是!!!,转化为大写/小写之后,是产生了一个新的对象

通过String类源码中的toUpperCase()方法和toLowerCase()方法返回的都是一个新的字符串。

image-20240107104504527

验证:

image-20240107104745011


3. 字符串转数组

public class Main {public static void main(String[] args) {// 字符串转数组String s1 = "hello";char[] chars = s1.toCharArray();for (char ch  : chars) {System.out.println(ch);}// 数组为字符串String s2 = new String(chars);System.out.println(s2);}
}

4. 格式化

public class Main {public static void main(String[] args) {String s1 = String.format("%d-%d-%d",2021,5,19);System.out.println(s1);}
}

1.5 字符串替换

使用一个指定的新的字符串替换掉已有的字符串数据,可用的方法如下:

方法说明
String replaceAll(String regex, String replacement)替换所有的指定内容
String replaceFirst(String regex, String replacement)替换收个内容
public class Main {public static void main(String[] args) {String s1 = "abcabcdeabcd";System.out.println(s1.replace('a', 'p')); // pbcpbcdepbcdSystem.out.println(s1.replace("ab","haha")); // hahachahacdehahacdSystem.out.println(s1.replaceAll("ab", "uuu")); // uuucuuucdeuuucdSystem.out.println(s1.replaceFirst("ab", "ha")); // hacabcdeabcd}
}

1.6 字符串拆分

可以将一个完整的字符串按照指定的分隔符划分为若干个子字符串。

方法功能
String[] split(String regex)将字符串全部拆分
String[] split(String regex, int limit)将字符串以指定的格式,拆分为limit组
public class Main {public static void main(String[] args) {String s1 = "name = zhangsan&age = 18";String[] strings = s1.split("&");for (int i = 0; i < strings.length; i++) {System.out.println(strings[i]);}String s2 = "Hello handsome hello beautiful give me some attention"; // 帅哥美女点点关注String[] strings1 = s2.split(" ",12); // 虽然不能分割12次 但是它能够保证能分割的最大次数 不够就不分了for (int i = 0; i < strings1.length; i++) {System.out.println(strings1[i]);}}
}

特殊情况:

public class Main {public static void main(String[] args) {String s1 = "192.168.1.2";String[] strings = s1.split("\\.");for (int i = 0; i < strings.length; i++) {System.out.println(strings[i]);}System.out.println("=========");String s2 = "C:\\APP\\Java\\jdk1.8\\bin\\java.exe";String[] strings1 = s2.split("\\\\");for (int i = 0; i < strings1.length; i++) {System.out.println(strings1[i]);}System.out.println("=========");String s3 = "name=zhangsan&age=18";String[] strings2 = s3.split("&|=");for (int i = 0; i < strings2.length; i++) {System.out.println(strings2[i]);}}
}

【注意事项】:

  1. 字符"|“,”*“,”+“都得加上转义字符,前面加上”\".
  2. 而如果是"“,那么就得写成”\\".
  3. 如果一个字符串中有多个分隔符,可以用"|"作为连字符.

多次拆分:

public class Main {public static void main(String[] args) {String s1 = "name=zhangsan&age=18";String[] strings = s1.split("&");for (String x:strings) {String[] strings2 = x.split("=");for (String x1 :strings2) {System.out.println(x1);}}}
}

1.7 字符串的截取

从一个完整的字符串之中截取出部分内容。可用方法如下:

方法功能
String substring(int beginIndex)从指定索引截取到结尾
String substring(int beginIndex, int endIndex)截取部分内容
public class Main {public static void main(String[] args) {String s1 = "helloworld" ;System.out.println(s1.substring(5)); // worldSystem.out.println(s1.substring(0, 5)); // hello 包含 0 下标的字符, 不包含 5 下标}
}

1.8 其他操作

方法功能
String trim()去掉字符串中的左右空格,保留中间空格

代码案例:trim()方法:

public class Main {public static void main(String[] args) {String str = "   hello  world   " ;System.out.println("["+str+"]");// [   hello  world   ]System.out.println("["+str.trim()+"]");// [hello  world]	}
}

trim 会去掉字符串开头和结尾的空白字符(空格, 换行, 制表符等).

2. StringBuilde 类 和 StringBuffer类

由于String的不可更改特性,为了方便字符串的修改,Java中又提供StringBuilderStringBuffer类。这两个类大 部分功能是相同的,这里介绍 StringBuilder常用的一些方法,其它需要用到了大家可参阅 [StringBuilder在线文档](Overview (Java Platform SE 8 ) (oracle.com))

方法功能
StringBuff append(String str)在尾部追加,相当于String的+=,可以追加:boolean、char、char[]、 double、float、int、long、Object、String、StringBuff的变量
char charAt(int index)获取index位置的字符
int length()获取字符串的长度
int capacity()获取底层保存字符串空间总的大小
void ensureCapacity(int mininmumCapacity)扩容
void setCharAt(int index, char ch)将index位置的字符设置为ch
int indexOf(String str)返回str第一次出现的位置
int indexOf(String str, int fromIndex)从fromIndex位置开始查找str第一次出现的位置
int lastIndexOf(String str)返回最后一次出现str的位置
int lastIndexOf(String str, int fromIndex)从fromIndex位置开始找str最后一次出现的位置
StringBuff insert(int offset, String str)在offset位置插入:八种基类类型 & String类型 & Object类型数据
StringBuffer deleteCharAt(int index)删除index位置字符
StringBuffer delete(int start, int end)删除[start, end)区间内的字符
StringBuffer replace(int start, int end, String str)将[start, end)位置的字符替换为str
String substring(int start)从start开始一直到末尾的字符以String的方式返回
String substring(int start,int end)将[start, end)范围内的字符以String的方式返回
StringBuffer reverse()反转字符串
String toString()将所有字符按照String的方式返回
public class Main {public static void main(String[] args) {StringBuilder sb1 = new StringBuilder("hello");StringBuilder sb2 = sb1;// 追加:即尾插-->字符、字符串、整形数sb1.append(' ');                  // hellosb1.append("world");              // hello worldsb1.append(123);                  // hello world123System.out.println(sb1);          // hello world123System.out.println(sb1 == sb2);   // trueSystem.out.println(sb1.charAt(0));   // 获取0号位上的字符  hSystem.out.println(sb1.length());    // 获取字符串的有效长度14System.out.println(sb1.capacity());  // 获取底层数组的总大小sb1.setCharAt(0, 'H');     // 设置任意位置的字符 Hello world123sb1.insert(0, "Hello world!!!");         // Hello world!!!Hello world123System.out.println(sb1);System.out.println(sb1.indexOf("Hello"));          // 获取Hello第一次出现的位置System.out.println(sb1.lastIndexOf("hello"));  // 获取hello最后一次出现的位置sb1.deleteCharAt(0);                               // 删除首字符sb1.delete(0,5);                                   // 删除[0, 5)范围内的字符String str = sb1.substring(0, 5);                  // 截取[0, 5)区间中的字符以String的方式返回System.out.println(str);sb1.reverse();                      // 字符串逆转str = sb1.toString();               // 将StringBuffer以String的方式返回System.out.println(str);}
}

从上述例子可以看出:StringStringBuilder最大的区别在于**String的内容无法修改**,StringBuilder的内容可 以修改。频繁修改字符串的情况考虑使用StringBuilder

注意:StringStringBuilder类不能直接转换。如果要想互相转换,可以采用如下原则:

  • String变为StringBuilder: 利用StringBuilder的构造方法或append()方法
  • StringBuilder变为String: 调用toString()方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/626117.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NLP论文阅读记录 - 2021 | WOS HG-News:基于生成式预训练模型的新闻标题生成

文章目录 前言0、论文摘要一、Introduction1.1目标问题1.2相关的尝试1.3本文贡献 二.相关工作三.本文方法四 实验效果4.1数据集4.2 对比模型4.3实施细节4.4评估指标4.5 实验结果4.6 细粒度分析 五 总结思考 前言 HG-News: News Headline Generation Based on a Generative Pre-…

【数位dp】【C++算法】600. 不含连续1的非负整数

作者推荐 【矩阵快速幂】封装类及测试用例及样例 涉及知识点 数位dp LeetCode600. 不含连续1的非负整数 给定一个正整数 n &#xff0c;请你统计在 [0, n] 范围的非负整数中&#xff0c;有多少个整数的二进制表示中不存在 连续的 1 。 示例 1: 输入: n 5 输出: 5 解释: 下…

刷题 ------ 二分枚举(查找)

文章目录 1.x 的平方根2.第一个错误的版本3.有效的完全平方数4.猜数字大小5.排列硬币6. 寻找比目标字母大的最小字母7. 二分查找8.检查整数以及其两倍数是否存在9. 两个数组间的距离值10.特殊的数组的特征值11.找出数组排序后的目标下标12.和有限的最长子序列13.正整数和负数的…

Day04

今日任务 24.两两交换链表中的节点19.删除链表的倒数第N个节点 160. 链表相交142.环形链表II 24 两两交换链表中的节点 题目链接&#xff1a;https://leetcode.cn/problems/swap-nodes-in-pairs/description/ 方法一&#xff1a;遍历实现 思路&#xff1a; 代码&#xff…

Linux/OpenAdmin

Enumeration nmap 用nmap扫描发现目标对外开放了22和80&#xff0c;端口详细信息如下 从nmap的结果看到&#xff0c;是apache的default page&#xff0c;使用工具跑一下目录&#xff0c;看了官 网文档的结果然后写个小字典节约时间&#xff0c;扫描结果如下 On the page at /…

new Handler(getMainLooper())与new Handler()的区别

Handler 在Android中是一种消息处理机制。 new Handler(); 创建handler对象&#xff0c;常用在已经初始化了 Looper 的线程中调用这个构造函数&#xff08;即非主线程&#xff09;&#xff0c;如果感觉不好理解&#xff0c;可以把Handler handler new Handler() 理解为常用在…

曲面上偏移命令的查找

今天学习老王的SW绘图时&#xff0c;遇到一个命令找不到&#xff0c;查询了一会终于找到了这个命令&#xff0c;防止自己忘记&#xff0c;特此记录一下&#xff0c;这个命令就是“曲面上偏移”&#xff0c;网上好多的教程都是错误的&#xff0c;实际上这个命令没有在曲面里面&a…

MySQL(三)——函数

上期文章 MySQL&#xff08;二&#xff09;——SQL 文章目录 上期文章字符串函数数值函数日期函数流程函数总结 函数&#xff1a;一段可以直接被另一段程序调用的程序或代码 字符串函数 函数功能CONCAT(S1,S2,…Sn)字符串拼接&#xff0c;将S1,S2,…Sn拼接成一个字符串LOWER…

快速前端开发01

前端开发 1 前端开发1.快速开发网站2.浏览器能识别的标签2.1 编码&#xff08;head&#xff09;2.2 title&#xff08;head&#xff09;2.3 标题2.4 div和span2.4.5 超链接2.4.6 图片小结2.4.7 列表2.4.8 表格2.4.9 input系列&#xff08;7个&#xff09;2.4.10 下拉框2.4.11 多…

Flask 项目怎么配置并创建第一个小项目?附上完成第一个小案例截图

目录 1. 为什么要学习 flask&#xff1f; 2. flask 是什么&#xff1f; 3. flask 如何使用&#xff1f; 要安装 Flask&#xff0c;可以按照以下步骤进行&#xff1a; 4. 使用流程 4.1. 新建项目 4.1.1. 打开 pycharm&#xff0c;新建项目 4.1.2. 设置目录&#xff0c;并…

MySql前言

&#x1f3a5; 个人主页&#xff1a;Dikz12&#x1f525;个人专栏&#xff1a;MySql&#x1f4d5;格言&#xff1a;那些在暗处执拗生长的花&#xff0c;终有一日会馥郁传香欢迎大家&#x1f44d;点赞✍评论⭐收藏 目录 数据库有哪些软件&#xff1f;&#xff1f; Mysql MySql数…

14.鸿蒙HarmonyOS App(JAVA)时钟组件计时器倒计时单选按钮复选框开关switch与开关按钮ToggleButton图像组件示范

鸿蒙HarmonyOS App(JAVA) 时钟组件 计时器 倒计时 单选按钮 复选框 开关switch 开关按钮ToggleButton 图像组件 ability_main.xml <?xml version"1.0" encoding"utf-8"?> <DirectionalLayoutxmlns:ohos"http://schemas.huawei.co…

HarmonyOS4.0系列——05、状态管理之@Prop、@Link、@Provide、@Consume,以及@Watch装饰器

状态管理 看下面这张图 Components部分的装饰器为组件级别的状态管理&#xff0c;Application部分为应用的状态管理。开发者可以通过StorageLink/LocalStorageLink 实现应用和组件状态的双向同步&#xff0c;通过StorageProp/LocalStorageProp 实现应用和组件状态的单向同步。…

关于群晖ARPL界面能出现ip但是使用Synology Assistant搜索不到ip问题 及解决方法

文章引用ing304 频道文章&#xff1a;https://qun.qq.com/qqweb/qunpro/share?_wv3&_wwv128&appChannelshare&inviteCode20jx8dPsU2z&contentID1m4NKs&businessType2&from181174&shareSource5&bizka 前言 当进入该界面后 提示IP无法访问&a…

【学习心得】图解Git命令

图解Git命令的图片是在Windows操作系统中的Git Bash里操作截图。关于Git的下载安装和理论学习大家可以先看看我写的另两篇文章。链接我放在下面啦&#xff1a; 【学习心得】Git快速上手_git学习心得-CSDN博客 【学习心得】Git深入学习-CSDN博客 一、初始化仓库 命令&#xff…

eBPF运行时安全

引言 eBPF作为当前linux系统上最为炙手可热的技术&#xff0c;通常被用于网络流量过滤和分析、系统调用跟踪、性能优化、安全监控&#xff0c;当下比较知名的项目有Cilium、Falco等。 Cilium 是一个开源的容器网络和安全性项目&#xff0c;致力于提供高效的容器通信和强大的安…

Java代码审计FastJson反序列化利用链跟踪动态调试autoType绕过

目录 0x00 前言 0x01 基础参考 JNDI注入实例 使用type加入User类解析 FastJson历史漏洞简介 0x02 FastJson 1.2.24 利用链分析 调试过程 构造Poc思路 CC链关键流程 0x03 FastJson 1.2.25-1.2.47 利用链分析 1、开启autoTypeSupport&#xff1a;1.2.25-1.2.41 调试过…

含并行连结的网络(GoogLeNet)

目录 1.GoogLeNet 2.代码 1.GoogLeNet inception不改变高宽&#xff0c;只改变通道数。GoogLeNet也大量使用1*1卷积&#xff0c;把它当作全连接用。 V3耗内存比较多&#xff0c;计算比较慢&#xff0c;但是精度比较准确。 2.代码 import torch from torch import nn from t…

MATLAB - 使用运动学 DH 参数构建机械臂

系列文章目录 前言 一、 使用 Puma560 机械手机器人的 Denavit-Hartenberg (DH) 参数&#xff0c;逐步建立刚体树形机器人模型。在连接每个关节时&#xff0c;指定其相对 DH 参数。可视化机器人坐标系&#xff0c;并与最终模型进行交互。 DH 参数定义了每个刚体通过关节与其父…

非常好用的Mac清理工具CleanMyMac X 4.14.7 如何取消您对CleanMyMac X的年度订购

CleanMyMac X 4.14.7是Mac平台上的一款非常著名同时非常好用的Mac清理工具。全方位扫描您的Mac系统&#xff0c;让垃圾无处藏身&#xff0c;您只需要轻松单击2次鼠标左键即可清理数G的垃圾&#xff0c;就这么简单。瞬间提升您Mac速度。 CleanMyMac X 4.14.7下载地址&#xff1a…