Java学习之操作符(三)

注:本博客内容是本人在看《Jave编程思想》这本书时从该书上抄录下来的一些片段。这里也强烈建议各位读者去购买这本书进行阅读学习。

一、Java操作符

操作符接受一个或多个参数,并生成一个新值。参数的形式与普通的方法调用不同,但效果相同。加号和一元的正号(+)、减号和一元的负号(-)、乘号(*)、除号(/)以及赋值号(=)的用法与其他编程语言一致。

操作符作用于操作数,生成一个新值。另外,有些操作符可能会改变操作数自身的值,这被称为“副作用”。那些能改变其操作数的操作符,最普遍的用途就是用来产生副作用;但要记住,使用此类操作符生成的值,与使用没有副作用的操作符生成的值没有什么区别。

几乎所有的操作符都只能操作“基本类型”。例外的操作符是"=",“==”和“!=”,这些操作符能操作所有对象。除此之外,String类型支持“+”和“+=”,用于字符串拼接。

二、优先级

当一个表达式中存在多个操作符时,操作符的优先级决定了各部分的计算顺序。Java对计算顺序做了特别的规定。其中,最简单的规则就是先乘除后加减。可以使用括号明确规定计算的顺序。使用括号时优先计算括号中的数据。例如:

@Test
public void precedence() {
   int x = 1, y = 2, z = 3;
   int a = x + y - 2 / 2 + z;
   int b = x + (y -2) / (2 + z);
   System.out.println("a = " + a);
   System.out.println("b = " + b);
}

输出结果

a = 5
b = 1

这两个语句看起来大体相同,但是输出结却大不相同,而这正是使用括号的结果。

三、赋值

赋值使用操作符“=”。它表示:取右边的值(即右值),把它复制给左边(即左值)。右值可以是任何常数、变量或者表达式(只要该表达式能生成一个值就行)。但是左值必须是明确的、已命名的变量。也就是说,必须有一个物理空间可以存储等号右边的值。举一个简单的例子,例如将一个常量赋值给一个变量:

a = 4;

但是不能把任何东西赋值给一个常量,常量不能作为左值(比如说不能说4=a)。

对于基本数据类型的赋值是很简单的。基本类型存储了实际的数值,而非指向一个对象的引用,所以在为其赋值的时候,是直接将一个地方的内容复制到另一个地方。例如,对基本数据类型使用a=b,那么b的内容就复制给a。若接着修改了a,而b根本不会受这种修改的影响。而这正是大多数情况下我们所期望的。

但是在为对象“赋值”的时候,情况发生了变化。对一个对象就行操作时,我们真正操作的的是对象的引用。所以倘若“将一个对象赋值给另一个对象”,实际上就是将“引用”从一个地方复制到另一个地方。这意味着假如对对象使用c=d,那么c和d都指向原本只有d指向的那个对象。示例代码如下:

定一个Task.java类:

public class Task {
    int level;
}

测试代码:

public class TaskTest {
    public static void main(String[] args) {
        Task task1 = new Task();
        Task task2 = new Task();
        task1.level = 20;
        task2.level = 40;
        System.out.println("task1.level = " + task1.level + ", task2.level = " + task2.level);
        // 将task2赋值给task1
        task1 = task2;
        System.out.println("task1.level = " + task1.level + ", task2.level = " + task2.level);
        // 修改task1
        task1.level = 22;
        System.out.println("task1.level = " + task1.level + ", task2.level = " + task2.level);
    }
}

输出结果:

task1.level = 20, task2.level = 40
task1.level = 40, task2.level = 40
task1.level = 22, task2.level = 22

该测试类非常简单。创建两个Task对象task1和task2,分别对它们的level赋予不同的值,然后将task2赋值给task1,紧接着又修改了task1.在许多编程语言中,我们可能期望task1和task2是相互独立的。但是由于赋值操作的是一个对象的引用,所以修改task1的同时也修改task2。这是由于task1和task2包含的是相同的引用,它们指向相同的对象。(原本task1包含的对对象的引用,是指向一个值为20的对象,但是在对task1赋值的时候,即task1=task2时,这个引用被覆盖(task1指向task2对象的引用),也就丢失了;而那个不再被引用的对象会被“垃圾回收器”自动清理)。

这种特殊的现象通常被称为“别名现象”,这是Java操作对象的一种基本方式。在该例中,若想避免别名现象问题该如何做呢?可以这样写:

public class TaskTest {
    public static void main(String[] args) {
        Task task1 = new Task();
        Task task2 = new Task();
        task1.level = 20;
        task2.level = 40;
        System.out.println("task1.level = " + task1.level + ", task2.level = " + task2.level);
        // 将taks1 = task2修改为 task1.level = task2.level;
        task1.level = task2.level;
        System.out.println("task1.level = " + task1.level + ", task2.level = " + task2.level);
        // 修改task1
        task1.level = 22;
        System.out.println("task1.level = " + task1.level + ", task2.level = " + task2.level);
    }
}

输出结果:

task1.level = 20, task2.level = 40
task1.level = 40, task2.level = 40
task1.level = 22, task2.level = 40

丛输出结果来看使用task1.level = task2.level的方式可以保持对象的彼此独立,而不是将task1和task2绑定到相同的对象。但是,这样直接操作对象内的域容易导致混乱,并且违背了良好的面向对象程序设计的原则。

方法调用中的别名现象

将一个对象传递给方法时,也会产生别名现像:例如:

创建一个简单的Letter对象:

public class Letter {
    char ch;
}

测试方法:

public class MothedTest {
    static void testReference(Letter letter) {
        letter.ch = 'x';
    }

    public static void main(String[] args) {
        Letter letter = new Letter();
        letter.ch = 'a';
        System.out.println("before invoke letter.ch = " + letter.ch);
        // 调用方法
        testReference(letter);
        System.out.println("after invoke letter.ch = " + letter.ch);

    }
}

测试结果:

before invoke letter.ch = a
after invoke letter.ch = x

在许多编程语言中,方法testReference()似乎要在它的作用域内复制其参数Letter letter的一个副本;但实际上只是传递了一个引用。所以代码行:

letter.ch = 'x';

实际上改变的是testReference()之外的对象。

别名现象引起的问题及其解决方法是比较复杂,这里先不做相关介绍。但是你至少应该知道它的存在,并在使用中注意这个陷阱。

四、算术操作符

Java的基本算术操作符与其他大多数编程语言是相同的。其中包括加号(+)、减号(-)、乘号(*)、除号(/)以及取模操作符(%,它从整数除法中产生余数)。整数除法会直接去掉结果的小数位,而不是四舍五入的结果

Java也使用一种来自C和C++的简化符号同时进行运算和赋值操作。这用操作符后紧跟一个等号来表示,它对Java中的所有操作符都适用。例如,要将x加4,并将结果赋回给x,可以这么些:x += 4。

下面一些例子展示了各种算术操作符的用法:

    @Test
    public  void multOperator () {
        Random random = new Random(47);
        int i, j, k;
        // 从1到100中取一个值
        j = random.nextInt(100) + 1;
        System.out.println("j = " + j);
        k = random.nextInt(100) + 1;
        System.out.println("k = " + k);
        // 加
        i = j + k;
        System.out.println("j + k = " + i);
        // 减
        i = j - k;
        System.out.println("j - k = " + i);
        // 乘
        i = j * k;
        System.out.println("j * k = " + i);
        // 除
        i = j / k;
        System.out.println("j/k = " + i);
        // 取模(或求余)
        i = j % k;
        System.out.println("j%k = " + i);
        // 浮点数测试
        float u, v, w;
        v = random.nextFloat();
        System.out.println("v = " + v);
        w = random.nextFloat();
        System.out.println("w = " + w);
        // 加
        u = v + w;
        System.out.println("v + w = " + u);
        // 减
        u = v -w;
        System.out.println("v - w = " + u);
        // 乘
        u = v * w;
        System.out.println("v * w = " + u);
        // 除
        u = v / w;
        System.out.println("v/w = " + u);
        // 运算与赋值操作符
        // +=
        u += v;
        System.out.println("u +=v :" + u);
        // -=
        u -= v;
        System.out.println("u -=v :" + u);
        // *=
        u *= v;
        System.out.println("u *=v :" + u);
        // /=
        u /= v;
        System.out.println("u /=v :" + u);
        // %=;
        i %= j;
        System.out.println("i %=j :" + i);
    }

输出结果如下:

j = 59
k = 56
j + k = 115
j - k = 3
j * k = 3304
j/k = 1
j%k = 3
v = 0.5309454
w = 0.0534122
v + w = 0.5843576
v - w = 0.47753322
v * w = 0.028358962
v/w = 9.940527
u +=v :10.471473
u -=v :9.940527
u *=v :5.2778773
u /=v :9.940527
i %=j :3

一元加、减操作符

一元减号(-)和一元加号(+)与二元减号和二元加号都使用相同的符号。根据表达式的书写形式,编辑器会自动判断出使用哪一种。例如:

        int a = 10;
        int x = -a;
        System.out.println("x = " + x);

输出结果:

x = -10

再看一下如下例子:

        int a = 10;
        int b = 2;
        
        x = a * -b;
        System.out.println("a * -b = " + x);

输出结果:

a * -b = -20

x = a * -b的写法虽然编辑器可以正确的识别,但是读者可能会被搞糊涂,所以有时可以明确地写成:

x = a * (-b);
System.out.println("a * (-b) = " + x);

其输出结果是一样的:

a * (-b) = -20

一元减号用于转变数据的符号,而一元加只是为了与一元减号相对应,但是它唯一的作用仅仅是将较小类型的操作数提升为int类型。

五、自动递增和递减

递增和递减运算是两种相当不错的快捷运算(通常被称为“自动递增”和“自动递减”运算)。其中递减操作符是“--”,意为“减少一个单位”;递增操作符是“++”,意为“增加一个单位”。例如:假设a是一个int类型的值,则表达式++a等价于(a = a +1)。递增和递减操作符不仅改变了变量,而且以变量的值作为生成的结果。

这两种操作符有两种用法,通常被称为“前缀式”和“后缀式‘。”前缀式递增“表示”++“操作符位于变量或表达式的前面;而”后缀式递增“表示”++“操作符位于变量或表达式的后面。类似地,”前缀式递减“意味着”--“操作符位于变量或表达式的前面;而”会缀式递减“意味着”--“操作符位于变量或表达式的后面。对于前缀式递增和前缀式递减(如++a或--a),回先执行计算,再生成值。而对于后缀式递增和后缀式递减(如a++或a--),会先生成值,再执行运算。例如:

   @Test
    public void  autoInc () {
        int i = 1;
        System.out.println("i :" + i); // 1
        System.out.println("++ i : " + ++ i); // 先自增 2
        System.out.println("i ++ : " + i ++); // 后自增 2
        System.out.println("i :" + i); // 3
        System.out.println("-- i : " + -- i); // 先自减 2
        System.out.println("i -- : " + i --); // 后自减 2
        System.out.println("i : " + i); // 1
    }

执行结果:

i :1
++ i : 2
i ++ : 2
i :3
-- i : 2
i -- : 2
i : 1

六、关系操作符

关系操作符生成的是一个boolean(布尔)结果,它们计算的是操作数之间的关系。如果关系是真实的的,关系表达式会生成true(真);如果关系不真实,则生成false(假)。关系操作符包括小于(<)、大于(>)、小于等于(<=)、大于等于(>=)、等于(==)以及不等于(!=)。等于和不等于适用于所有的数据类型,而其他比较符不适用于boolean类型。因为boolean值只能为truefalse。“大于”和“小于”没有实际意义。

测试对象的等价性

关系操作符==和!=也适用于所有对象,但是这两个操作符通常会使第一次接触Java的程序员感到迷惑。例如:

    @Test
    public void equalsObject() {
        Integer t1 = new Integer(50);
        Integer t2 = new Integer(50);
        System.out.println("t1 == t2 :" + (t1 == t2));
        System.out.println("t1 != t2 :" + (t1 != t2));
        
    }

输出结果:

t1 == t2 :false
t1 != t2 :true

一些读者可能会认为上面程序输出的结果应该先是true,再是false,因为两个Integer对象都是相同的。但是尽管对象的内容相同,然而对象的引用却是不同的,而==和!=比较的就是对象的引用。所以输出结果实际上先是false,再是true。

如果想要比较两个对象的实际类容是否相同,又该如何操作呢?此时,必须使用所有对象都有的特殊方法equals()。但是这个方法不适用于“基本数据类型”,基本数据类型直接使用==和!=即可。如:

   @Test
    public void equalsObject() {
        Integer t1 = new Integer(50);
        Integer t2 = new Integer(50);
        System.out.println("t1的值和t2的值是否相当?" + t1.equals(t2));
    }

输出结果:

t1的值和t2的值是否相当?true

结果正如我们所预料的那样。但事情并不总是这么简单。假设你创建了自己的类。如下这样:

    class Value {
        int v;
    }

    @Test
    public void compareObject() {
        Value value1 = new Value();
        Value value2 = new Value();
        value1.v = value2.v = 100;
        System.out.println(value1.equals(value2));
    }

输出结果:

flase

事情再次变得让人费解了:结果又是false,而不是true!这是由于equals()方法的默认行为是比较引用。所以除非在自己的新的类中覆盖equals()方法,否则不可能表现出我们希望的样子。至于如何重写覆盖equals()方法,后续章节再继续介绍。这里你需要注意equals()方法的这种行为表现方式,这样或许能够避免一些“灾难”。

七、逻辑操作符

逻辑操作符“与”(&&)、“或”(||)、非(!)能够根据参数的逻辑关系,生成一个布尔值(true或false)。“与”、“或”、“非”操作只可应用于布尔值。

逻辑操作符的短路现象

当我们使用逻辑操作符的时候,我们会遇到一种“短路”现象。即一旦能够明确确认整个表达式的值,就不会在计算表达式余下的部分了。因此,整个逻辑表达式靠后的部分有可能不会被运算。以下是一个简单的短路现象例子:

    @Test
    public void shortCircuit() {
        System.out.println(1 == 1 && 1 > 2 && test01());
    }

    public boolean test01() {
        System.out.println("3 > 4 ?");
        return 3 > 4;
    }

输出结果:

false

正常情况下如果三个测试语句都会执行。那么肯定会输出“3 > 4”这句字符串,但是从输出结果来看,并没有输出“3 > 4”。这里第一个判断1 ==1 等式成立,所以表达式会继续执行下去。然而第一个测试结果产生了一个false结果。由于这是三个“于”(&&)进行运算,有一个为false,那么就可以肯定表达式最终结果是false,所以没有必要继续执行下去,那样只是浪费。“短路”一词正是源于此。事实上,如果所有的逻辑表达式都有一部风不必计算,那么将获得潜在的性能提升。

八、直接常量

一般来说,如果在程序里使用“直接常量”,编译器可以准确地知道要生成什么样的类型,但有时候却是模凌两可的。如果发生这种情况,必须对编译器加以适当的“指导”,用与直接常量相关联的某些字符来额外增加一些信息。例如:

    @Test
    public void literals () {
        int t1 = 0x2f;
        System.out.println("t1 :" + Integer.toBinaryString(t1));
        int t2 = 0X2f;
        System.out.println("t2 :" + Integer.toBinaryString(t2));
        int t3 = 0177;
        System.out.println("t3 :" + Integer.toBinaryString(t3));
        char c = 0xffff;
        System.out.println("c :" + Integer.toBinaryString(c));
        byte b = 0x7f;
        System.out.println("b :" + Integer.toBinaryString(b));
        short s = 0x7fff;
        System.out.println("s :" + Integer.toBinaryString(s));
        long l1 = 200;
        long l2 = 200l;
        long l3 = 200L;
        System.out.println("l3 :" + l3);
        float f1 = 1;
        float f2 = 1f;
        float f3 = 1F;
        System.out.println("f3 :" + f3);
        double d1 = 1d;
        double d2 = 1D;
        System.out.println("d2 :" +d2);
    }

输出结果如下:

t1 :101111
t2 :101111
t3 :1111111
c :1111111111111111
b :1111111
s :111111111111111
l3 :200
f3 :1.0
d2 :1.0

直接常量后面的后缀字符标记了它的类型。若为大写(或小写)的L,代表的long(但是使用小写的字面l容易造成混淆,因为它看起来像数字1,所以建议使用大写L)。大写(或小写)字母F,代表float;大写(或小写)字母D,代表double。十六进制数适用于所有整数数据类型,以前缀0x(0X),后面跟随0-9会小写(或大写)的a-f(A-F)来表示。八进制数由前缀0以及后续的0~7的数字来表示。

在C、C++和Java中,二进制数没有直接常量表示方法。但是使用十六进制和八进制记数时,以二进制形式显示结果非常有用。通常使用Integer和Long类型的静态方法toBinaryString()方法可以很容易实现这一点。请注意,如果将较小的类型传递给Integer.toBinaryString()方法,则该类型将自动转为int。

九、按位操作符

按位操作符用来操作基本数据类型中的单个“比特”(bit),即二进制位。按位操作符会对两个参数中对应的位执行布尔代数运算,并最终生成一个结果。

如果两个输入位都是1,则按位“与”操作符(&)生成一个输出位1;否则生成一个输出位0(即“与“与”操作符只有1 & 1才等于1,其他的都为0);如果两个输入为里只要有一个是1,则按位“或”操作符(|)生成一个输出为1,只有在两个输入位都为0时,它才会生成一个输出为0;如果输入位的某一个是1,但不全都是1,那么按位“异或”操作(^)生成一个输出为1,有且仅当两个输入位都是1是,按位“异或”才输出一位0;按位“非”(~),也称为取反操作符,它属于一元操作符,只对一个操作数进行操作(其他按位操作符都是二元操作符),按位“非”生成与输入位相反的值——如输入0,则输出1,若输入1,则输出0。

按位操作符可以与等号(=)联合使用,以便合并运算赋值:&=、|=和^=都是合法的(由于‘~“是一元操作符,所以不可以和“=”联合使用)。

我们将布尔类型作为一种单比特值对待,所以它有点特殊。我们可以对布尔类型的值(true和false)执行按位“与”、按位“或”和按位“异或”,但不能执行按位“非”(大概是为了避免与逻辑NOT混淆)。对于布尔值,按位操作符具有与逻辑操作符相同的效果,只是它们不会“短路”。

十、位移操作符

位移操作符的运算对象也是二进制的“位”。位移操作符只可用来处理整数类型。左位移操作符(<<)能够按照操作符右侧指定的位数将操作符左边的操作数向左移动(在低位补0)。“有符号”右移(>>)则按照操作符右侧指定的位数将操作符左边的操作数向右移动。“有符号”右移位操作符使用“符号扩展”:若符号位正,则在高位补0;若符号为负,则在高位补1。Java中增加了一种“无符号”右移位操作符(>>>),它使用“零扩展”:无论正负,都在高位插入0。这是C和C++没有的。

”位移“可以和等号“=”组合使用(<<=或>>=或>>>=)。此时,操作符左边的值会移动由右边的指定的位数,再将得到的结果赋值给左边的变量。但是在进行“无符号”右移位结合赋值操作时,可能会遇到一个问题:如果对byte或short值进行这样的位移运算,得到的可能不是正确的结果。它们会被先转成int类型,在进行右移操作,然后被截断,赋值给原来的类型,在这种情况下可能得到一个-1的结果。

十一、三元操作符

三元操作符也称为条件操作符,它显得比较特殊,因为它有三个操作数;但它确实属于操作符的一种,因为它最终也会生成一个值。其表达形式如下:

boolean-exp ? value0 : value1;

如果boolean-exp(布尔表达式)的结果为true,就计算value0,而且这个计算结果也就是操作符最终产生的值;如果boolean-exp(布尔表达式)的结果是false,就计算value1,同样,它的结果也就成了最终操作符产生的值。例如:

    @Test
    public void ternaryIfElse() {
        int result = 1 > 2 ? 100 : 200;
        System.out.println("result = " +result);
    }

执行结果:

result = 200

十二、字符串操作符+和+=

在Java中++=有一项特殊的用途:连接不同的字符串。字符串操作符有一些有趣的行为。如果表达式以一个字符串开头,那么后续的所有操作数都必须是字符串类型(请记住,编译器会把双引号内的字符序列自动转成字符串)。例如:

    @Test
    public void stringOperator() {
        int x = 1, y = 2, z = 3;
        String str = "x , y , z ";
        System.out.println(str + x + y + z);
        str += "hello ";
        System.out.println(str + (x + y + z));
    }

输出结果:

x , y , z 123
x , y , z hello 
x , y , z hello 6

请注意,第一个打印结果输出得是x , y , z 123,而不是x , y , z 6(1 + 2 + 3 = 6),之所以出现这个结果是因为操作符“+”的起始是一个字符串,所以编译器就会将x,y,z的值转换成它们的字符串形式,然后连接这些字符串。最有一个之所以输出结果为6,是因为使用括号来控制表达式的赋值顺序,以使得int类型的变量在显示之前确实进行了求和运算。

十三、类型转换操作符

类型转换(cast)的愿意是“模型铸造”。在适当的时候,Java会将一种数据类型自动转换成另一种。例如,假设我们为某浮点数赋予一个整数值,编译器会将int自动转换成float。类型转换运算应许我们显示地进行这种转换,或者在不能自动转换的时候强制进行类型转换。

要想执行类型转换,需要将希望得到的类型置于圆括号内,放在要进行类型转换的值的左边。例如:

    @Test
    public void casting () {
        int i = 200;
        long lng = (long) i;
        long l = 100;
        int j = (int) l;
    }

需要注意的是,如果要执行一种名为窄化转换的操作(也就是说,将能够容纳更多信息的数据类型转换成无法容纳那么多信息的类型),就有可能面临信息丢失的危险。此时,编译器会强制我们进行类型转换,这实际上是说:“这可能是一件危险的事情,如果无论如何都要这么做,必须显示进行类型转换”。而对于扩展转换,则不必显示地进行类型转换,因为新的类型肯定能够容纳原来的类型,不会造成任何信息丢失。

Java允许我们把任何基本数据类型转换成别的其他的基本数据类型,但布尔类型除外,后者根本不允许进行任何类型的转换处理。

(1)截尾和舍如

在执行窄化转换时,必须注意截尾和舍入问题。例如,如果将一个浮点值转换成整型值,Java会进行如何处理?例如将23.7转换成int,结果会是23还是24?看代码如下:

   @Test
    public void castingNumber() {
        double value = 23.7, value1 = 23.4;
        float fl = 0.7f, lowe = 0.4f;
        System.out.println("(int) value = " + (int) value);
        System.out.println("(int) value1 = " + (int) value1);
        System.out.println("(int) fl = " + (int) fl);
        System.out.println("(int) lowe = " + (int)(lowe));
    }

输出结果:

(int) value = 23
(int) value1 = 23
(int) fl = 0
(int) lowe = 0

因此答案是在将floatdouble类型转换成int类型是,总是对该数执行截尾操作。如果想要达到四舍五入的值,就需要使用java.lang.Math中的roound()方法。如:

    @Test
    public void mathRound() {
        double value = 23.7, value1 = 23.4;
        float fl = 0.7f, lowe = 0.4f;
        System.out.println("Math.round(value) = " + Math.round(value));
        System.out.println("Math.round(value1) = " + Math.round(value1));
        System.out.println("Math.round(fl) = " + Math.round(fl));
        System.out.println("Math.round(lowe) = " + Math.round(lowe));
    }

输出结果:

Math.round(value) = 24
Math.round(value1) = 23
Math.round(fl) = 1
Math.round(lowe) = 0

(2)提升

如果对基本数据类型进行算术运算或者按位运算,大家就会发现,只要类型比int小(即charbyte或者short),那么在运算之前,这些值会自动转换成int。这样一来,最终生成的结果就是int类型。如果想要把结果赋值给较小的类型,就必须使用类型转换(既然把结果赋给了较小的类型,就可能出现信息丢失)。通常,表达式中出现的最大的数据类型决定了表达的最终结果的数据类型。如果将一个float值与一个double值相乘,结果就是double;如果将一个int和一个long值相加,则结果就是long

在Java中若不使用直接常量做显示声明,默认定义的整数的数据类型是int类型,小数默认是double类型。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值