BUAAOO P1-P3 Expression Dirivation

1.问题描述

求包含简单幂函数和简单正余弦函数的表达式的导函数

1.1.概念定义

  • 带符号整数: 支持前导 0 的带符号整数,符号可忽略
  • 因子
    • 变量因子
      • 幂函数
        • 一般形式 由自变量x和指数组成,指数为一个带符号整数
        • 省略形式 当指数为1的时候,可以采用省略形式
      • 三角函数 sin(x)cos(x)
        • 一般形式 类似于幂函数,由sin(x)cos(x) 和指数组成,指数为一个带符号整数
        • 省略形式 当指数为1的时候,可以采用省略形式,省略指数部分
    • 常数因子 包含一个带符号整数
    • 表达式因子 将在表达式的相关设定中进行详细介绍。表达式因子不支持幂运算。
    • 嵌套因子 支持因子嵌套在三角函数因子里面,即一个因子作为另一个三角函数因子的自变量,
    • 一般形式由乘法运算符连接若干任意因子组成
    • 特殊形式
      • 第一个因子为常数因子 1 且其后跟着乘号的时候,可以省略该常数因子或表示为正号开头的形式
      • 第一个因子为常数因子 -1 且其后跟着乘号的时候,可以表示为负号开头的形式
  • 表达式 由加法和减法运算符等若干项组成。此外,在第一项之前,可以带一个正号或者负号
    • 注意:
      • 表达式因子,表达式可以作为因子,其定义为被一对小括号包裹起来的表达式
      • 空串不属于合法的表达式
  • 空白字符 空白字符包含且仅包含<space>\t

    1.2.输入与输出

    输入: 一行表达式
    输出: 一行, 求导后的表达式. 若输入表达式非法则输出"WRONG FORMAT!"

    1.3.问题分析

    问题主要分为三部分:
  • 构造
    • 判定输入有效性
    • 将输入转换为便于处理的数据结构
  • 求导
    • 按照规则嵌套求导
    • 解决求导前后表达式结构变化的问题
  • 化简
    • 合并同类项

      2.解决历程

      本次作业经过了三个Project

      2.1.P1: 初步设计

      2.1.1.实现

  • 构造
    • 使用正则表达式匹配输入
    • 划分为不同层次的数列存储
  • 求导
    • 按照规则进行求导
    • 不能很好地处理求导前后表达式结构的变化
  • 化简
    • 乘法与加法合并同类项

      2.1.2.类型设计

      ToBeDone

      2.1.3.优缺点

  • 优点
    • 层次清晰
  • 缺点
    • 采用有序容器的序关系来确定可加性与可乘性, 这与数据的相等关系不一致, 导致逻辑混乱
    • 代码量大
    • 代码复用性欠佳
    • 采用正则表达式匹配处理输入, 导致输入处理复杂易错

      2.2.P2: 误入歧途

      尝试在第一次设计的基础上进一步层次化. 然而, 我并没有想到运用树形结构来解决求导前后表达式结构的变化的方法.

      2.2.1.实现

  • 构造
    • 使用正则表达式匹配输入
    • 划分为不同层次的数列存储
  • 求导
    • 按照规则进行求导
    • 不能很好地处理求导前后表达式结构的变化
  • 化简
    • 乘法与加法合并同类项

      2.2.2.类型设计

      ToBeDone

      2.2.3.优缺点

  • 优点
    • 层次清晰
  • 缺点
    • 采用有序容器的序关系来确定可加性与可乘性, 这与数据的相等关系不一致, 导致逻辑混乱
    • 代码量大
    • 代码复用性欠佳
    • 采用正则表达式匹配处理输入, 导致输入处理复杂易错

      2.3.P3: 茅塞顿开

      2.3.1.实现

  • 构造
    • 使用递归下降法处理输入
    • 构造为可求导对象树
  • 求导
    • 按照规则进行求导
    • 支持嵌套求导
    • 能很好地处理求导前后表达式结构的变化
  • 化简
    • 乘法与加法合并数字项

      2.3.2.类型设计

      ToBeDone

      2.3.3.优缺点

  • 优点
    • 层次清晰
    • 不依赖于序关系
    • 代码量小
    • 代码复用性高
    • 简单巧妙地解决了构造于求导问题
  • 缺点
    • 不利于化简

      3.类型设计

      ToBeDone

      4.实现分析

      4.1.构造: 递归下降语法分析

  1. 定义基础语法元素 (token): 不可再分的字符串
POSI := "\\+"
NEGE := "\\-"
MUL := "\\*"
POW := "\\^"
LPAR := "\\("
RPAR := "\\)"
SIN := "sin"
COS := "cos"
VAR := "x"
NUM := "\\d+"
SPACE := "[ \t]*"
  1. 递归定义语法对象 (exp)
EXP := (POSI|NEGE)?TERM((POSI|NEGE)TERM)*
TERM := (POSI|NEGE)?FAC(MUL FAC)*
FAC := LPAR EXP RPAR|POWFAC|SNUM
POWFAC := (VAR|SINFAC|COSFAC)(POW SNUM)?
SINFAC := SIN LPAR FAC RPAR
COSFAC := COS LPAR FAC RPAR
SNUM := (POSI|NEGE)?NUM
  1. 根据基础语法元素定义, 设计Token类

  2. 根据语法对象定义, 设计各语法对象生成函数

  3. 进行语法分析
    1. 拆分为token列表
      1. 若EOE则退出循环
      2. 若无匹配则语法错误
    2. 对列表进行语法分析, 进而生成语法树
    3. 凡生成Exp后tokenList非空或非右括号, 则语法错误
    4. 凡原子元素字符缺失, 或类型无匹配, 则语法错误

      4.2.求导

  • 对树上节点分别按规则求导
  • Exp
    • $ (f(x) + g(x))' = f'(x) + g'(x) $: rstExp = new Exp(this.leftTerm.dirivate(), this.op, this.rightExp.dirivate())
    • simplify: rstExp = rstExp.simplify()
  • Term
    • $ (f(x) * g(x))' = f'(x) * g(x) + f(x) * g'(x) $:
      • Dirivatable leftTerm = new Term(this.leftFac.dirivate(), this.op, this.rightTerm.clone());
      • Dirivatable rightExp = new Term(this.leftFac.clone(), this.op, this.rightTerm.dirivate());
      • Dirivatable rstExp = new Exp(leftTerm, "+", rightExp)
    • rstExp = rstExp.simplify()
  • Fac
    • Pow Fac:
      • $ f(x) ^ n = n * f(x) ^ (n - 1) * f'(x) $:
        • Dirivate rstExp = Exp.generate(new Tokens(this.index.toString() + "" + this.base.toString() + "^" + this.index.subtract(new Num("1")).toString() + "" + this.base.dirivate().toString()))
      • rstExp = rstExp.simplify()
    • Sin Fac:
      • $ (sin(f(x)))' = cos(f(x)) * f'(x) $:
        • Dirivate rst = Exp.generate(new Tokens("cos(" + this.base.toString() + ")*" + this.base.dirivate().toString()))
      • rst = rst.simplify()
    • Cos Fac:
      • $ (cos(f(x)))' = -sin(f(x)) * f'(x) $:
        • Dirivate rst = Exp.generate(new Tokens("-sin(" + this.base.toString() + ")*" + this.base.dirivate().toString()))
      • rst = rst.simplify()
  • Element
    • Num
      • return new Num("0")
    • Var
      • return new Num("1")

        4.3.化简

  • Exp
    • if ((leftTerm instanceof Num) && (rightExp instanceof Num)): return leftTerm [+-] rightExp
    • if (((leftTerm|rightExp) instanceof Num) && (leftTerm|rightExp).equals(new Num("0")): return (rightExp|leftTerm).clone()
    • else: return new Exp(this.leftTerm.simplify(), this.op, this.rightExp.simplify())
  • Term
    • if ((leftFac instanceof Num) && (rightTerm instanceof Num)):
      • return leftFac * rightTerm
    • if (((leftFac|rightTerm) instanceof Num) && (leftFac|rightTerm).equals(new Num("0"))):
      • return new Num("0")
    • if (((leftFac|rightTerm) instanceof Num) && (leftFac|rightTerm).equals(new Num("1"))):
      • return (rightTerm|leftFac).clone()
    • else:
      • return new Term(this.leftFac.simplify(), this.op, this.rightTerm.simplify())
  • Fac
    • Pow Fac:
      • if ((this.index instanceof Num) && index.equals(new Num("0"))):
        • return new Num("1")
      • if ((this.index instanceof Num) && index.equals(new Num("1"))):
        • return this.base.simplify()
      • else:
        • return new PowFac(this.base.simplify(), this.op, this.index.simplify())
    • Sin Fac:
      • if ((this.base instanceof Num) && this.base.equals(new Num("0"))):
        • return new Num("0")
      • else:
        • return new SinFac(this.op, this.base.simplify())
    • Cos Fac:
      • if ((this.base instanceof Num) && this.base.equals(new Num("0"))):
        • return new Num("1")
      • else:
        • return new CosFac(this.op, this.base.simplify())

          5.难点分析

  • 输入处理
    • 对于嵌套的输入, 不使用递归下降法, 只使用正则表达式将无法处理
    • 使用正则表达式和类有限状态机将大大提高逻辑复杂度, 且不利于代码模块化
  • 数据结构与求导
    • 如果不使用树形结构, 而使用分层的线性结构存储, 将无法处理求导前后的表达式结构变化问题: 一个Term在求导后会变为一个Expression, 这样的结构转换在线性存储结构中是无法实现的

      6.BUG分析

      6.1.爆栈

      如果使用正则表达式直接匹配, 则会出现爆栈的情况.
      原因是贪婪搜索启用了回溯, 将占用大量的栈空间.
      解决方案: 鉴于本问题中不必考虑回溯匹配, 可采用占有搜索取代贪婪搜索

      6.2.求导格式问题

      Expression:
  • +1 x^-1
    Expected Output:
    x^-2
    Output:
    -1
    -1*x
    出错原因在于CosFac类中:
    @Override
    public Dirivatable dirivate()
    {
        if (!(this.index instanceof Num))
        {
            throw new RuntimeException("Index of pow is not a num");
        }
        Num index = (Num) this.index;
        try
        {
            return Exp.generate(new Tokens(this.index.toString() +
                "*(" + this.base.toString() + ")^" +
                ((Num) this.index).subtract(new Num("1")).toString() +
                "*" + this.base.dirivate().toString())).simplify();
        } catch (SytaxError sytaxError)
        {
            throw new RuntimeException(sytaxError.getMessage());
        }
    }

其中"*(" + this.base.toString() + ")^"对幂函数的底数项使用了小括号, 使其被认为是一个表达式因子. 而表达式因子是不能有幂函数的, 故导致出错. 改为"*" + this.base.toString() + "^"即可.

7.程序度量

ToBeDone

8.知识点笔记

[TOC]

1.运行

javac <FileName>
java <ClassName>

注意:

  • 源代码文件名主名必须与公有类名一致
  • 源代码后缀名为java
  • 类名后不要加".java"
  • 从main方法开始运行

    2.字符串

  • Unicode字符序列
  • 使用双引号表示: "This is a string."
  • 使用加号+表示拼接
    • 拼接时非字符串被转换为字符串
    • 可以使用类属join方法拼接
  • 字符串不能修改, 只能创建新串
  • 使用format方法格式化创建字符串
String mystr = "This is my string.";
String merged = "String 1" + "String 2";//拼接
String rating = "PG" + 13;//自动类型转换
String allSize = String.join("/", "S", "M", "L");//第一个参数是分隔符, 后面的参数是被分隔字符串
String newString = mystr.substring(0, 8) + "a new string.";//创建新串
String message = String.format("Hello, %s.", name);//格式化创建字符串

2.1.方法

mystr.length();//返回字符串长度
String mySubString = mystr.substring(0, 3);//返回子串
String allSize = String.join("/", "S", "M", "L");//第一个参数是分隔符, 后面的参数是被分隔字符串
aString.equals(anotherString);//检测相等性
aString.equalsIgnoreCase(anotherString);//检测相等性 (忽略大小写)
aString.compareTo(anotherString)==0;//另一种检测相等性

2.2.检测相等性

使用equals方法

aString.equals(anotherString);//检测相等性

不要使用"=="判相等

2.3.空串与null串

空串是长度为0, 内容为空的字符串

if (str.length() == 0)

null是特殊的, 表示没有任何对象与该变量关联的值

if (str == null)

先检查是否为null, 再检查是否为空串

2.4.使用StringBuilder构建字符串

StringBuilder builder = new StringBuilder();//创建新StringBuilder
builder.append(aString);//在末位拼接字符串
String myString = builder.toString();//转换为字符串

2.5.使用正则表达式判断和修改内容

  • 右大括号"}"不需要转义
aString.matches(aRegexStr); // 使用正则表达式判断内容
aString.replaceAll(aRegexStr, aReplacement); // 使用正则表达式替换全部
aString.replaceFirst(aRegexStr, aReplacement); // 使用正则表达式替换第一个匹配串
String[] splitStrs = aString.split(aRegexStr);//返回以正则表达式匹配串为间隔分离出的字符串数组

2.6.转换为字符串

  • 使用toString()方法转换为字符串
    • toString()方法声明在Object类中, 但推荐所有类Override该方法

      3.IO

      3.1.输入

      使用Scanner类
  • 定义在java.util包中
import java.util.*;

Scanner in = new Scanner(System.in);//新建Scanner
String line = in.nextLine();//读取一行
String word = in.next();//读取一个word
int num = in.nextInt();//读取一个整数
double doubleNum = in.nextDouble();//读取一个浮点数

in.hasNext();//判断是否有下一个word
in.hasNextInt()://判断是否有下一个int
in.hasNextDouble();//判断是否有下一个double

3.2.输出

System.out.print();//不添加换行符
System.out.println();//自动添加换行符
System.out.printf();//格式化输出

3.3.文件IO

3.3.1.文件输入
public static void main(String[] args) throws IOException
{
    Scanner in = new Scanner(Paths.get("c:\\mydirectory\\myfile.txt"), "UTF-8");//注意对反斜杠进行转义
}
3.3.2.文件输出

public static void main(String[] args) throws IOException
{
    PrintWriter out = new PrintWriter("myfile.txt", "UTF-8");
    out.close();//注意关闭 (刷新) 输出流
}

4.控制结构

4.1.块作用域

4.2.条件语句

if (a == b)
{
    //do something
}
else if (a == c)
{
    //do something else
}
else
{
    //do something else
}

switch (choice)
{
    case option1:
    {
        //do something
        break;//break用于跳出选择或循环
    }
    default:
    {
        //do something
        break;
    }
}

4.3.循环语句

while (a == b)
{
    //do something
}

do
{
    //do something
} while (a == b);

for (int i = 0; i < n; i++)
{
    //do something
}//i的声明到此为止

for (int num : aArray)
{
    //do something
}

4.4.跳转语句

while (a == b)
{
    //do something
    break;
}

label:
while (a == b)
{
    //do something
    break label;//跳出被label标记的语句
}

while (a == b)
{
    //do something
    continue;
}

5.标识符

由字母, 美元符, 下划线和数字组成, 且不由数字开头.

5.1.类名

大写字母开头, 驼峰命名法.

5.2.常量名

全大写.

6.方法

6.1.调用

  • 方法得到的是参数值的拷贝
  • 方法不能修改基本数据类型的参数
  • 方法可以改变对象类型的参数的状态, 但不能让对象类型的参数引用新的对象
    • foreach循环也类似
  • 动态绑定
    • 从本类向超类搜索对应签名方法
    Package.Object(CLass).Methode();

    7.注释

    //One line comment
    /*
    Multi-line comment
    */
    /**
     *java-doc comment
     */

    7.1.javadoc

    由源文件抽取注释生成HTML文档
    从以下几个特性中抽取信息
  • 公有类与接口
  • 公有的和受保护的方法
  • 公有的和受保护的域
    应为以上几部分编写javadoc注释
  • 注释应放在所描述对象的前面
  • 以/**开始, 并以*/结束
  • 第一句应是一个概要性句子
  • 在标记后紧跟着自由格式文本(由空白符分隔)
    • 标记由@开始
    • 可以使用html修饰符, 但不能使用<hl><hr>
    • 使用{@code ... }输入代码

      7.1.1.通用标记
  • @author作者
  • @version版本
  • @since
  • @deprecated弃用
  • @see package.class#feature label引用

    7.1.2.类注释
  • import语句之后, 类定义之前

    7.1.3.方法注释
    除通用标记外, 还可使用
  • @param参数
  • @return返回值
  • @throws异常

    7.1.4.域注释
    7.1.5.包注释

    package-info.java

    7.1.6.生成文档
javadoc -d <docDirectory> <SourceCodeFile>

8.数据类型

8.1.整型

类型位宽
byte8
short16
int32
long64
  • long字面量: 100L
  • 十六进制字面量: 0xAFE
  • 八进制字面量: 010
  • 二进制数: 0b1001
  • 可以使用下划线: 0b1101_1001_1010_1011
  • 没有unsigned型

    8.2.浮点型

    类型位宽
    float32
    double64
  • float字面量: 3.14F
  • double字面量(默认): 3.14(或3.14D)

    8.3.boolean类型

    仅能为true或false

    8.4.变量

    8.4.1.声明
int vacationDays;
8.4.2.初始化
int vacationDays = 12;
8.4.3.常量
final double PI=3.14;

类属常量:

public class Constant
{
    private static final double PI = 3.14;
    
    public static void main(String[] args)
    {
        //do something
    }
}

8.5.类型转换

8.5.1.强制类型转换
double x = 3.14;
int y = (int) x;

8.6.char型

  • 用于表示Unicode码元
  • 使用单引号表示: 'A'

    8.7.枚举类型

enum Size { SMALL, MEDIUM, LARGE }; // 定义枚举类型
Size aSize = Size.MEDIUM; // 声明枚举类型变量并初始化

9.数学函数与常量

double x = 4;
double y = Math.sqrt(x);
Math.PI;

导入Math类所有名称:

import java.lang.Math.*;

y = sqrt(x);

10.运算符

10.1.普通运算符

x = a + b;

10.2.运算赋值符

x += 3;

10.3.自增自减运算符

x++;

10.4.关系运算符

x == y;
x != y;
x <= y;
(x == y) && (a != b);
! (x == y);

10.5.三目运算符

x < y ? x : y;

10.6.位运算符

y = x & 0b1001;

int fourthBitFromRight = (n & (1 << 3)) >> 3;

10.7.枚举类型

enum Size { SMALL, MEDIUM, LARGE };

Size aSize = Size.MEDIUM;

11.包

  • 嵌套的包之间没有任何关系, 每一个都是独立的类的集合

    11.1.默认导入

impoer java.lang.*;

11.2.类的导入

  • 一个类可以使用
    • 所属包中的所有类
    • 其他包中的公有类
      • 在每个类名之前添加完整包名
      • 导入类
        • 导入类后, 只用写类名, 不用写包名
        • import package.*;
        • import package.class;
      • 导入静态方法与静态域
        • 导入静态方法与静态域后, 只用写静态方法与静态域名, 不用写包名和类名
          ```
          java.time.LocalDate.now();

import java.time.LocalDate;
LocalDate.now();

### 11.3.将类放入包中
- 将包的名字放在源文件开头
    - 如果没有放置package语句, 则自动放入default package中
- 将文件放入与完整包名匹配的 (相对编译基目录的) 路径中
### 11.4.包作用域
- public可以被任意类使用
- 未标记 (默认) 可以被同一个包中的类使用
- private只能被本类使用
### 11.5.类路径
编译器和虚拟机搜索类的列表
## 12.大数值
### 12.1.BigInteger

BigInteger aBigInteger = BigInteger.valueOf(100);//创建BigInteger
BigInteger aBigInteger = new BigInteger(numStr);//创建BigInteger
BigInteger aBigInteger = new BigInteger(numStr, aRadix);// 以特定基数创建BigInteger
BigInteger sum = aBigInteger.add(bBigInteger);//加法
BigInteger mul = aBigInteger.multiply(bBigInteger);//乘法
BigInteger inputNum = in.nextBigInteger();//输入BigInteger
String ans = aBigInteger.toString();//转换为字符串
String ans = aBigInteger.toString(aRadix);//转换为特定基数的字符串

## 13.数组
- 初始化时, 数值自动初始化为0, boolean自动初始化为false, 对象自动初始化为null

int[] aArray;//声明数组aArray
int[] aArray = new int[100]//声明数组aArray并初始化为100个元素的数组
int[] aArray = {1, 2, 3, 4};//声明并初始化
new int[] { 1, 2, 3, 4 };//初始化匿名数组
aArray = new int[] { 1, 2, 3 };//不创建新变量而重新初始化数组
System.out.println(aArray[1]);//调用数组元素
aArray.length;//取得数组长度
Arrays.toString(aArray);//转换为字符串

### 13.1.数据域

aArray.length;//取得数组长度

### 13.2.方法

Arrays.toString(aArray);//转换为字符串
int[] anotherArray = Arrays.copyOf(aArray, aArray.length);//克隆
Arrays.sort(aArray);//排序

### 13.3.数组拷贝
拷贝引用, 两个变量引用同一个数组

int[] anotherArray = aArray;

克隆

int[] anotherArray = Arrays.copyOf(aArray, aArray.length);//克隆
int[] anotherArray = Arrays.copyOf(aArray, aArray.length * 2);//改变数组长度

### 13.4.排序

int[] aArray = new int[1000];
...
Arrays.sort(aArray);//排序

### 13.5.多维数组
元素为数组的数组

int[][] magicSquare =
{
{ 1, 2, 3, 4 },
{ 1, 2, 3, 4 },
{ 1, 2, 3, 4 },
{ 1, 2, 3, 4 }
};

for (int[] row : magicSquare)
{
for (int num : row)
{
//do something
}
}

int[][] odds = new int[rowNum][];
for (int i = 0; i <= MAX; i++)
{
odds[i] = new int[i + 1];
}

## 14.命令行参数

java <args[0]> <args[1]>[ ...]

## 15.类
- 类: 构造对象的模板
- 类的实例: 由类构造的对象
- 实例域: 对象中的数据
- 方法: 对象中操作数据的过程
    - 构造器: 构造并初始化对象
    - 访问器: 访问实例域
    - 更改器: 修改实例域
- 状态: 对象中实例域的集合
- 封装: 对象的使用者仅能通过对象的方法访问对象的实例域
- 识别类
    - 名词是类
    - 动词是方法
- 不返回可变对象的引用, 只返回它的克隆, 否则会破坏封装性
- 一个方法可以访问所属类的所有对象的私有数据

public class Employee
{
private String name;
private double salary;
private LocalDate hireDay;

public Employee(String n, double s, int year, int month, int day)
{
    name = n;
    salary = s;
    hireDay = LocalDate.of(year, month, day);
}

public String getName()
{
    return name;
}

public void raiseSalary(double byPercent)
{
    double raise = salary * byPercent / 100;
    salary += raise;
}

}

### 15.0.使用预定义类

Date birthday = new Date();//使用构造器
LocalDate newYearsEve = LocalDate.of(1999, 12, 31);//使用静态工厂方法
newYearsEve.getYear();//访问器方法

### 15.1.类间的方法
- 依赖(uses-a)
- 聚合(has-a)
- 继承(is-a)
### 15.2.构造器
- 名字应与类名相同
- 没有返回值
- 使用new关键词构造新对象
- 不能对已经存在的对象调用
- 不要声明与实例域重名的变量, 会屏蔽实例域
- 一个类可以有多个构造器
- 可以显式初始化实例域
- 构造器可以在首行调用其他构造器
- 没有被初始化的值会被初始化为默认值(0或null)
- 若没有编写构造器, 会提供无参数的默认构造器
- 初始化块
    - 首先运行初始化块, 再运行构造器
### 15.3.隐式参数

number.raise(7);//隐式参数: number, 显式参数: 7

使用this表示隐式参数

this.salary += raise;

### 15.4.类属域与类属方法
- 使用static关键字表示
- 类属方法不能访问实例域, 仅能访问类属域
### 15.5.类设计技巧
1. 数据私有
2. 数据初始化
3. 不要使用过多基本类型, 将它们打包为单独的类
4. 不是所有域都需要访问器和更改器
5. 将职责过多的类拆分
6. 类名和方法名要清晰体现职责
7. 优先使用不可变类
8. 类的可变性
    1. 若含有可变对象, 可以在不改变引用的情况下改变值, 则本类为可变类
    2. 否则为不可变类
9. 设计任何类, 均需要考虑重写三个方法:
    1. toString(): 必须实现
    2. clone(): 默认为浅拷贝, 只拷贝域中对象的引用
        1. 若实现Clonable接口, 且本类为可变类, 则需重写为深拷贝 (拷贝每个不可变对象的引用, 克隆每个可变对象)
    3. equals(): 默认比较引用, 引用相同则相同 (等价于运算符"==")
        1. 若为不可变对象, 则需重写为比较值
        2. 若为可变对象, 则不需重写 (若为可变对象, 引用不同, 值相同, 规定不能equals, 因为之后值可能变化为不同)
## 16.对象
数据和方法的集合.
对象的三个主要特征:
- 行为 (方法)
- 状态 (实例域)
- 标识 (标识符)

注意: 变量是对象指针, 变量不是对象
## 17.重载
多个同名方法有不同的参数列表
## 18.正则表达式
使用java.util.regex包
- 栈
    - 以左括号顺序标号

import java.util.regex.Matcher;
import java.util.regex.Pattern;

String content = "This is a content string.";
String patternString = "\d";

Pattern aPattern = Pattern.compile(patternString);

Matcher aMatcher = aPattern.matcher(content);

aMatcher.find();//进行一次匹配

System.out.println(aMatcher.group(0));//输出匹配串
System.out.println(aMatcher.group(1));//输出捕获组1的匹配串

System.out.println(content.substring(aMatcher.start(), aMatcher.end()));//输出匹配串

String aReplacement = "Replacement";
System.out.println(aMatcher.replaceAll(aReplacement));//输出替换后字符串

StringBuffer aStrBuffer = new StringBuffer();
while(aMatcher.find()) {
aMatcher.appendReplacement(aStrBuffer, Replacement);//多次替换
}
aMatcher.appendTail(aStrBuffer);
System.out.println(aStrBuffer.toString());//输出替换结果

StringBuffer aStrBuffer = new StringBuffer();
if(aMatcher.find()) {
aMatcher.appendReplacement(aStrBuffer);//一次替换
}
aMatcher.appendTail(aStrBuffer);System.out.println(aStrBuffer.toString());//输出替换结果

### 18.1.Pattern类
- 创建: Pattern.compile(patternStr)
- 匹配: Pattern.matches(pattern, content)
### 18.2.Matcher类
#### 18.2.1.创建
- 创建: aPattern.matcher(contentStr)
#### 18.2.2.匹配
- 尝试匹配包含第一个字符的串: aMatcher.lookingAt()
- 查看目前有多少个捕获组: aMatcher.groupCount()
    - group(0)总是代表整个表达式
- 进行一次匹配并返回匹配真值: aMatcher.find()
    - 重置匹配器并从指定下标的字符开始匹配: aMatcher.find(startIndex)
- 尝试匹配整个输入: aMatcher.matcher()
- 匹配过程与量词模式
    - 匹配过程
        - 按照运算优先级, 从左到右依次匹配, 左侧模式匹配后再匹配右侧
    - 量词模式
        - (默认) 贪婪
            - 匹配第一个字符, 若不满足, 则失配, 返回
            - 若下一个字符满足模式且未达到量词限制, 则匹配下一个字符, 且重复本步; 若下一个字符不满足模式, 则本模式匹配成功, 进行下一个模式的匹配
            - 若下一个模式失配, 且本模式剩余字符数大于最小字符数, 则吐出最后匹配的字符, 本模式匹配成功, 进行下一个模式的匹配
        - (?) 厌恶
            - 匹配第一个字符, 若不满足, 则失配, 返回
            - 本模式匹配成功, 进行下一个模式的匹配
            - 若下一个模式失配, 且本模式未达到匹配限制, 且下一个字符满足模式, 则匹配下一个字符, 本模式匹配成功, 进行下一个模式的匹配; 若下一个模式失配, 且 (本模式达到匹配限制, 或下一个字符不满足模式), 则失配
        - (+) 占有
            - 匹配第一个字符, 若不满足, 则失配, 返回
            - 若下一个字符满足模式且未达到量词限制, 则匹配下一个字符, 且重复本步; 若下一个字符不满足模式, 则本模式匹配成功, 进行下一个模式的匹配
            - 若下一个模式失配, 则失配 (不会回溯搜索)
#### 18.2.3.捕获
- 返回上次匹配存储的捕获组内容: aMatcher.group()
- 返回上次匹配的匹配串初始字符下标: aMatcher.start()
    - 返回上次匹配的捕获组匹配串初始字符下标: aMatcher.start(groupId)
- 返回上次匹配的匹配串结尾字符之后字符的下标: aMatcher.end()
    - 返回上次匹配的捕获组匹配串结尾字符之后字符的下标: aMatcher.end(groupId)
    - 命名捕获组 (也加入捕获组计数): `(?<<GroupName>><PatternStr>)`; 获取命名捕获组内容: `matcher.group("<GroupName>")`
#### 18.2.4.修改
- 替换每个匹配串并返回结果: aMatcher.replaceAll(replacement)
- 替换第一个匹配串并返回结果: aMatcher.replaceFirst(replacement)
- 替换匹配串, 并把从上次替换的位置到本次替换的位置之间的字符串与本次替换的结果加入StringBuffer中: aMatcher.appendReplacement(aStringBuffer, replacement)
- 把最后一次替换之后的所有字符加入StringBuffer中: aMatcher.appendTail(aStringBuffer)
#### 18.2.5.重置
重新赋值

aMatcher = aPattern.matcher(content);

## 19.断言
断言 (Assertion)
- 程序执行时该表达式应该为true
    - 针对致命的, 不可恢复的错误
    - 只用于开发和测试阶段
- 使用assert关键字声明
    - `assert <condition>;`
    - `assert <condition> : <expression>;`
    - `<condition>`是布尔表达式
    - 运行时会对条件`<condition>`进行检测, 若结果为false, 则抛出`AssertionError(<expression>)`异常
- 仅在测试期间生效, 发布时会自动移除
    - 默认禁用
    - 可通过-ea或-enableassertions选项启用默认包中的类的断言: `java -ea MyClass`
        - 可对某个类或包及其子包启用: `java -ea:MyClass -ea:com.mycompany.mylib MyClass`
    - 不必重新编译
## 20.系统
- 使用System.exit(0);退出程序
    - 使用System.exit(-1);会导致RuntimeError
## 21.数据容器
### 21.1.Comparator
- 接口
- 按其中compare返回值升序排序
    - `(x, y)`<==>`aComparator.compare(x, y)<=0`
- 建议同时在被比较的类中Override equals方法, 确保equals与compare==0等价, 以避免不一致的冲突
### 21.2.Comparable
- 接口
- 按compareTo返回值升序排序
    - `(x, y)`<==>`x.compareTo(y)<=0`
- 建议同时在被比较的类中Override equals方法, 确保equals与compareTo()==0等价, 以避免不一致的冲突. 重载时注意重写equals(Object), 注意不要出现重载
### 21.3.Iterator
- 对有序集合 (有natural order), 枚举顺序有序, 按natural order (由实现Comparable接口的comtareTo方法定义)
- 对无序集合, 枚举顺序不保证有序
- 使用Iterator枚举过程中, 不能直接修改集合元素, 会导致Iterator抛出异常. 只能用Iterator操作元素

aIterator.hasNext(); // 若有剩余元素, 则返回true
aIterator.next(); // 返回下一个元素的引用
aIterator.remove(); // 从集合中删除最近返回的元素

### 21.4.LinkedList

import java.util.LinkedList;
LinkedList oneLinkedList = new LinkedList(); // 创建LinkedList对象
oneLinkedList.add(aStr); // 添加元素
oneLinkedList.add(aIndex, aStr); // 添加元素至指定位置
oneLinkedList.set(aIndex, aStr); // 设置元素
oneLinkedList.indexOf(aStr); // 返回值第一次出现的位置, 若不存在则返回-1
oneLinkedList.lastIndexOf(aStr); // 返回值最后一次出现的位置, 若不存在则返回-1
oneLinkedList.size(); // 返回元素个数
oneLinkedList.subList(firstIndex, lastIndex); // 返回指定 (含firstIndex, 不含lastIndex) 子列的引用. 对返回子列的状态修改会改变主列状态
oneLinkedList.clear(); // 清空列表
oneLinkedList.getFirst(); // 返回第一个元素
oneLinkedList.addFirst(aStr); // 在开头添加元素
oneLinkedList.addLast(aStr); // 在末尾添加元素
oneLinkedList.getLast(); // 返回最后一个元素

// 使用foreach语句枚举
for (String aStr : oneLinkedList)
{
// do something
}

- 使用Comparator排序

class Node
{
private int value;

Node(int value)
{
    this.value = value;
}

@Override
boolean equals(Node anotherNode)
{
    return this.value == anotherNode.value;
}

int getValue(void)
{
    return this.value;
}

}

class NodeComparator implements Comparator
{
@Override
int compare(Node node1, Node node2)
{
return node1.value-node2.value;
}
}

class NodeTest
{
public static void main(String[] args)
{
LinkedList nodeList = new LinkedList(); // 定义LinkedList变量
nodeList.add(new Node(1));
nodeList.add(new Node(2));
Collections.sort(nodeList, new NodeComparator()); // 排序
}
}

- 使用Comparable排序

class Node implements Comparable < Node >
{
private int value;

Node(int value)
{
    this.value = value;
}

@Override
int compareTo(Node anotherNode)
{
    return this.value-anotherNode.value;
}

int getValue(void)
{
    return this.value;
}

}

class NodeTest
{
static void main(String[] args)
{
LinkedList nodeList = new LinkedList(); // 定义LinkedList变量
nodeList.add(new Node(1));
nodeList.add(new Node(2));
Collections.sort(nodeList); // 排序
Collections.sort(nodeList, Collections.reverseOrder()); // 逆序
}
}

### 21.5.HashMap

import java.util.HashMap;

class HashMapTest
{
publit static void main(String[] args)
{
HashMap<Integer, Integer> aHashMap = new HashMap<Integer, Integer>(); // 创建HashMap对象
aHashMap.put(,); // 添加键值对 (key-value entry), 若key已经存在, 则返回旧value, 并将新value存储在key对应元素中; 若key不存在, 则返回null, 并将新value存储在key对应元素中
aHashMap.putIfAbsent(,); // 若key存在且对应原有value不为null, 则不会覆盖原有value, 并返回原有value; 若key不存在或key存在且对应原有value为null, 则存储新value, 并返回null
aHashMap.remove(); // 若存在key, 删除键值对并返回value, 否则返回null
aHashMap.remove(, ); // 匹配则删除entry并返回true, 否则返回false
() aHashMap.get(); // 获取key对应的value的引用
aHashMap.getOrDefault(, ); // 获取key对应value, key不存在则返回指定的默认值
aHashMap.containsKey(); // 查询是否存在key
aHashMap.containsValue(); // 查询是否存在value
aHashMap.replace(, ); // 若key存在, 则返回旧value, 存储新value; 若key不存在, 则什么都不做

    // 元素遍历
    Iterator aIterator = aHashMap.entrySet().iterator(); // 获取枚举器
    while (aIterator.hasNext())
    {
        Map.Entry aEntry = (Map.Entry) aIterator.next();
        Integer key = (Integer) aEntry.getKey();
        Integer value = (Integer) aEntry.getValue();

        // 或
        Map.Entry<Integer, Integer> aEntry = (Map.Entry <Integer, Integer>) aIterator.next();
        Integer key = aEntry.getKey();
        Integer value = aEntry.getValue();
    }
}

}

- Override hashCode方法以控制key类型对应的hashCode
    - 默认使用内存地址

class Element
{
int number;

@Override
int hashCode()
{
    return number;
}

}

### 21.6.TreeMap
- 基于红黑树实现, containsKey, get, put, remove均保证log(n)时间复杂度
- 类型参数必须实现Comparable接口或被指定Comparator接受

TreeMap<Integer, Integer> aTreeMap = new TreeMap <Integer, Integer>(); // 创建
aTreeMap.put(, ); // 加入键值对

// 遍历
Set <Map.Entry <Integer, Integer>> aEntrySet = aTreeMap.entrySet();
for (Map.Entry <Integer, Integer> aEntry : aEntrySet)
{
Integer key = aEntry.getKey();
Integer value = aEntry.getValue();
}

aTreeMap.size(); // 返回元素个数
Integer aInteger = (Integer) aTreeMap.get(); // 获取对应的value
Integer aInteger = (Integer) aTreeMap.firstKey();
Integer aInteger = (Integer) aTreeMap.lastKey();
Integer aInteger = (Integer) aTreeMap.lowerKey(); // 返回小于指定值的最大key
Integer aInteger = (Integer) aTreeMap.ceilingKey(); // 返回大于等于指定值的最小key
SortedMap <Integer, Integer> aSortedMap = aTreeMap.subMap(, ); // 取出指定区间的子TreeMap
aTreeMap.remove(); // 删除键值对
aTreeMap.clear(); // 清空集合元素
aTreeMap.isEmpty(); // 判断是否为空
aTreeMap.containsKey();

### 21.7.TreeSet
- 实现SortedSet接口的Set, 通过元素的类所实现的Comparatable接口所定义的natural order排序

TreeSet treeSet = new TreeSet<>();
treeSet.add(aElement);
treeSet.contains(aElement);
treeSet.remove(aElement);
treeSet.clear();
treeSet.ceiling(aElement); // 返回集合中大于等于给定元素的最小元素, 若不存在则返回null
treeSet.floor(aElement); // 返回集合中小于等于给定元素的最大元素, 若不存在则返回null
treeSet.size(); // 返回元素个数
Interator aInterator = treeSet.iterator(); // 返回升序枚举器
while (aInterator.hasNext())
{
System.out.println(aInterator.next());
}

### 21.8.自定义数据容器
- 实现`Iterable<T>`以允许foreach循环
### 21.9.Queue

import java.util.LinkedList;
import java.util.Queue;

Queue queue = new LinkedList(); // 初始化
queue.offer(); // 添加元素, 成功则返回true, 否则返回false
queue.peek(); // 返回但不弹出队首. 若队列为空则返回null
queue.poll(); // 返回并弹出队首, 若队列为空则返回null

## 22.泛型
- 以类型作为参数
- 使用尖括号表示类型参数: `<<Type>>`
### 22.1.泛型类
- 接受类型作为参数
- 在类名后添加类型参数列表
    - 可包含一个及以上类型参数, 用逗号分隔
    - 每个类型参数是一个指代被传入的实际类型的标识符
- 每传入一组实际的类型参数, 就创建了一个具体的类
        - 可以传入通配符类型参数来创建所有传入具体类型参数所创建的类的父类: `AClass<?>`
        - 可以在通配符类型参数上加入允许的类型参数的范围的上下限 (`? extends <上限>`和`? super <下限>`)
- 类型参数只能代表引用型类型, 不能代表原始类型 (int, double, char)

// 泛型类的定义
class AClass {
private AType aObject;

void add(AType aObject) {
    this.aObject = aObject;
}

AType get() {
    return this.aObject;
}

}

public class GenericTest {
public static void main(String[] args) {
AClass aStr = new AClass(); // 创建泛型类对象
AClass aInt = new AClass();
AClass<?> aObj;
AClass<? extends Integer> aInt;
AClass<? super Number> aObj;

    aStr.add(new String("Test"));
    aInt.add(new Integer(10));
    
    System.out.println(String(aStr.get()));
    System.out.println(String(aInt.get()));
}

}

### 22.2.泛型方法
- 接受类型参数的方法
- 类型参数列表放在方法返回类型之前
    - 可以有一或多个, 用逗号分隔
    - 每个类型参数是一个指代被传入的实际类型的标识符
- 类型参数只能代表引用型类型, 不能代表原始类型 (int, double, char)

public class GenericMethodTest {
public static void printArray(AType[] inputArray) {
for (AType element : inputArray) {
System.out.println(element);
}
}

public static void main(String[] args) {
    Integer[] intArray = { 1, 2, 3, 4 };
    printArray(intArray);
}

}

## 23.接口
- 超级抽象类
    - 方法 (功能) 的集合
    - 比抽象类更抽象: 所有方法都必须是抽象的
    - 只有方法声明, 没有方法定义, 方法定义在实现 (implements) 接口的具体类中
    - 意义
        - 处理单继承 (一个类只能有一个父类) 的局限性
        - 降低程序耦合性
- 使用interface关键字定义
- 不能使用new创建 (实例化) 对象
- 只能定义类属常量, 必须使用修饰符public static final
- 只能定义抽象方法, 必须使用修饰符public abstract
- 接口的子类必须实现所有抽象方法后, 才能成为具体类 (否则是抽象类), 才能创建 (实例化) 对象
- 关系
    - 类与类
        - 继承关系
        - 单继承: 一个类只能有一个直接父类
        - 多层继承
    - 接口与接口
        - 继承关系
        - 多继承: 一个接口可以有多个直接父接口
    - 类与接口
        - 实现关系
        - 多实现: 一个类可以实现多个接口

public interface AInterface {
public static final ACONST = 10;

public abstract method();

}

class AClass implements AInterface, AnotherInterface {
@Override
public void method() {
// do something
}
}

实现了接口的类的对象可调用接口中的方法

AClass object = new AClass();
object.method(); // 调用接口方法

## 24.Random

import java.util.Random;

Random aRandom = new Random();
aRandom.nextInt();

## 25.Integer

Integer aInteger = new Integer();

## 26.类
- 若需要使用clone(), 则必须实现Clonable接口并实现clone()方法, 最好实现深拷贝
## 27.异常
- 异常 (对象) 是Throwable类的子类的实例
    - 可恢复错误
    - 向程序其他部分和用户通告错误的方式
### 27.1.抛出异常
- 在方法声明处标明可能抛出的异常
- 以下情况会抛出异常
    - 调用抛出异常的方法 (应声明)
    - 程序运行时发现错误, 利用throw语句抛出受查异常 (应声明)
    - 程序出现错误, 抛出非受查异常
    - Java系统出现错误

() throws ,
{
// Do something
if (Error)
{
throw new ();
}
}

### 27.2.创建异常类
- 创建异常类
    - 继承Exception或其子类
    - 创建无参构造器和字符串构造器

class FileFormatException extends IOException
{
public FileFormatException(){}
public FileFormatException(String errMsg)
{
super(errMsg);
}
}

### 27.3.Throwable类
- 使用Throwable类对象

new Throwable(); // 创建对象
Throwable throwable = new Throwable(errMsgStr); // 以错误描述信息创建对象
throwable.getMessage(); // 获取错误描述信息

### 27.4.捕获异常
- 针对抛出的异常, 可以选择捕获 (Catch) 或继续抛出
- 若main方法抛出异常, 则程序会终止执行并打印错误信息
- 异常捕获
    - 若未抛出异常, 则跳过catch块, 执行finally块, 执行try-catch-finally块之后的代码
    - 若抛出异常
        - 遍历所有catch块
            - 若存在对应块, 则跳至对应块处理, 处理结束后执行finally块, 再继续执行try-catch-finally块之后的代码
                - 可以在catch块中再次抛出异常
        - 若不存在对应块, 则执行finally块, 再继续抛出异常
- 建议: 早抛出, 晚捕获

public class AClass
{
public static void main(String[] args)
{
try
{
// Do something
}
catch ( exception)
{
// Handle
exception.getMessage(); // 得到错误信息
exception.getClass().getName(); // 得到实际类型
exception.printStackTrace(); // 打印堆栈信息
StackTraceElement[] stackMsgs = exception.getStackTrace(); // 获取堆栈信息数组
for (StackTraceElement stackMsg : StackMsgs)
{
System.out.println(stackMsg); // 打印堆栈信息
}
throw exception;
throw new RuntimeException(exception.getMessage()); // 抛出非受查异常
}
catch ( | anotherException)
{
// Handle
}
}
}

- 针对实现了AutoCloseable接口的类的资源, 可使用带资源的try语句
    - try退出时, 将自动调用`res.close()`

try (Resource res = ...; Resource anotherRes = ...)
{
// Do something
}

## 28.日志

Logger.getGlobal(); // 取得全局日志记录器
Logger.getGlobal().info(); // 向全局日志记录器中写入INFO级信息
Logger.getGlobal().setLevel(Level.OFF); // 设置全局日志记录器记录级别

## 29.继承
- 子类访问父类的私有变量: 通过public或protected访问器与更改器
-  instanceof通过返回一个布尔值来指出,这个对象是否是这个类或者是它的超类的一个实例
## 30.静态工厂方法
- 在抽象类中使用静态工厂方法替代构造器, 以针对不同输入返回不同子类对象
## 31.获取对象所属类
- 获取对象真实类: `obj.getClass().getName();`
## 32.递归下降语法分析
1. 定义基础语法元素 (token): 不可再分的字符串

POSI := "\+"
NEGE := "\-"
MUL := "\"
POW := "\^"
LPAR := "\("
RPAR := "\)"
SIN := "sin"
COS := "cos"
VAR := "x"
NUM := "\d+"
SPACE := "[ \t]
"


2. 递归定义语法对象 (exp)

EXP := (POSI|NEGE)?TERM((POSI|NEGE)TERM)
TERM := (POSI|NEGE)?FAC(MUL FAC)

FAC := LPAR EXP RPAR|POWFAC|SNUM
POWFAC := (VAR|SINFAC|COSFAC)(POW SNUM)?
SINFAC := SIN LPAR FAC RPAR
COSFAC := COS LPAR FAC RPAR
SNUM := (POSI|NEGE)?NUM


3. 根据基础语法元素定义, 设计Token类
4. 根据语法对象定义, 设计各语法对象生成函数
5. 进行语法分析
   1. 拆分为token列表
      1. 若EOE则退出循环
      2. 若无匹配则语法错误
   2. 对列表进行语法分析, 进而生成语法树 (本例中语法树转换为值)
   3. 凡生成Exp后tokenList非空或非右括号, 则语法错误
   4. 凡原子元素字符缺失, 或类型无匹配, 则语法错误
## 33.树

public interface Tree {

/**
 * 判空
 * @return
 */
boolean isEmpty();

/**
 * 二叉树的结点个数
 * @return
 */
int size();

/**
 * 返回二叉树的高度或者深度,即结点的最大层次
 * @return
 */
int height();

/**
 * 先根次序遍历
 */
String preOrder();

/**
 * 中根次序遍历
 */
String inOrder();

/**
 * 后根次序遍历
 */
String postOrder();

/**
 * 层次遍历
 */
String levelOrder();

/**
 * 将data 插入
 * @return
 */
void insert(T data);

/**
 * 删除
 */
void remove(T data);

/**
 * 查找最大值
 * @return
 */
T findMin();

/**
 * 查找最小值
 * @return
 */
T findMax();

/**
 * 根据值找到结点
 * @param data
 * @return
 */
BinaryNode findNode(T data);

/**
 * 是否包含某个值
 * @param data
 * @return
 */
boolean contains(T data) throws Exception;

/**
 * 清空
 */
void clear();

}

public class BinaryNode
{
public BinaryNode left;//左结点

public BinaryNode<T> right;//右结点

public T data;

public BinaryNode(T data,BinaryNode left,BinaryNode right){
    this.data=data;
    this.left=left;
    this.right=right;
}

public BinaryNode(T data){
    this(data,null,null);

}

/**
 * 判断是否为叶子结点
 * @return
 */
public boolean isLeaf(){
    return this.left==null&&this.right==null;
}

}

public class BinarySearchTree implements Tree {
//根结点
protected BinaryNode root;

public BinarySearchTree(){
    root =null;
}
@Override
public boolean isEmpty() {
    return false;
}

@Override
public int size() {
    return 0;
}

@Override
public int height() {
    return 0;
}

@Override
public String preOrder() {
    return null;
}

@Override
public String inOrder() {
    return null;
}

@Override
public String postOrder() {
    return null;
}

@Override
public String levelOrder() {
    return null;
}

@Override
public void insert(T data) {
if (data==null)
throw new RuntimeException("data can'Comparable be null !");
//插入操作
root=insert(data,root);
}

/**

  • 插入操作,递归实现
  • @param data
  • @param p
  • @return
    */
    private BinaryNode insert(T data,BinaryNode p){
    if(p==null){
    p=new BinaryNode<>(data,null,null);
    }

    //比较插入结点的值,决定向左子树还是右子树搜索
    int compareResult=data.compareTo(p.data);

    if (compareResult<0){//左
    p.left=insert(data,p.left);
    }else if(compareResult>0){//右
    p.right=insert(data,p.right);
    }else {
    ;//已有元素就没必要重复插入了
    }
    return p;
    }

    @Override
    public void remove(T data) {

    }

    @Override
    public T findMin() {
    return null;
    }

    @Override
    public T findMax() {
    return null;
    }

    @Override
    public BinaryNode findNode(T data) {
    return null;
    }

    @Override
    public boolean contains(T data) throws Exception {
    return false;
    }

    @Override
    public void clear() {

    }
    }
    ```

转载于:https://www.cnblogs.com/daniel10001/p/10610307.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值