【java提高】---细则(4)

java提高(16)---java注解

注解含义注解是JDK1.5之后才有的新特性,它相当于一种标记,在程序中加入注解就等于为程序打上某种标记,之后又通过类的反射机制来解析注解。

一、JDK自带注解

JDK1.5之后内部提供的三个注解

 @Deprecated       #废弃,过时。
 @Override         #重写、覆盖。
 @SuppressWarnings #压缩警告。

示例

@SuppressWarnings("deprecation")
public class AnnotationTest {
    //4、这里称为压缩警告注解,可以在类上也可以放在方法上,因为该方法用了个已经过期的方法.getYear(),所以会发出警告
    //加上这个注解表明取消对deprecation的警告,那么该方法里有过时方法也不会发出预警。同时getYear()的那条横线也消失了。
    @SuppressWarnings("deprecation")
    public static void main(String[] args) {

   //1、这里的.getYear()方法画了一条横线表示此方法已经过时了,里面方法加上了@Deprecated注解
        new Date().getYear();
    }

    //2、这里我通过@Deprecated注解自定义一个已经过时不建议使用的方法。
    @Deprecated
    public String   getName() {
        return "小小";

    }
    //3、重写(覆盖)父类Object的toString()方法
    @Override
    public String toString() {
        return "小小";
    }
}

注解示意图

 

二、自定义注解

示例

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface AnCode {//使用@interface关键字定义注解

    //如果只有一个属性 强烈建议取名为value
    String value() default "";
}										

在自定义注解上面有四个注解,我们称为元注解,下面一个一个解释。

1、@Target 用于描述注解的使用范围

取值(ElementType)有:
    1、CONSTRUCTOR:   用于描述构造器
    2、FIELD:         用于描述域(字段申明)
    3、LOCAL_VARIABLE:用于描述局部变量
    4、METHOD:        用于描述方法
    5、PACKAGE:       用于描述包
    6、PARAMETER:     用于描述参数
    7、TYPE:          用于描述类、接口(包括注解类型) 或enum声明
    8、TYPE_PARAMETER:输入参数申明(JDK1.8)
    9、TYPE_USE:      使用类型(JDK1.8)

2、@Retention 定义了该注解生命周期

取值(RetentionPoicy)有:
    1、SOURCE:  在源文件中有效(即源文件保留)
    2、CLASS:   在class文件中有效(即class保留)
    3、RUNTIME: 注解永久保留,可以被VM加载时加载到内存中

一般框架注解和我们自定义注解采用的几乎都是RUNTIME,因为只有这个才能运行时通过反射来获取注解中的数据。

3、@Inherited

概念: @Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。

注意:@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation
解释: 比如A继承B,B类的上面有一个注解@A带有元注解@Inherited 那么A也可以拥有B父类的这个注解@A,但接口实现是不可以的。同时需要指出@A注解是需要元注解@Retention(RetentionPolicy.RUNTIME)。

参考文章:java @Inherited注解的作用

4、@Documented

概念:描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。

5、自定义注解参数

  1、只能用public或默认(default)这两个访问权修饰.例如,String value();这里把方法设为defaul默认类型;  
      2、参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和
           String,Enum,Class,annotations等数据类型,以及这一些类型的数组;
   3、如果只有一个参数成员,最好把参数名称设为"value",后加小括号(也可以不加小括号)

 

三、自定义注解案例

目标 实现一个简单的通过注解生成SQL查询语句。

先创建两个注解

@Table表名注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {

    //数据库表名属性
    String value() default "";
}

@Column字段名注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    //数据库表字段名称和实体属性映射
    String value() default "";
}

User实体

@Table("t_user")
public class User {
    /**
     * 用户Id
     */
    @Column("user_id")
    private Integer userId;
    /**
     * 用户年龄
     */
    @Column("age")
    private Integer age;
    
    public User(Integer userId, Integer age) {
        this.userId = userId;
        this.age = age;
    }
    //添加get和set方法
    }

解析注解类

public class Query {
    public static String query(Object object) throws Exception{
        StringBuilder sql = new StringBuilder();
        //1.利用反射获取Class
        Class c = object.getClass();
        //2.获取Table的名字
        boolean isExist = c.isAnnotationPresent(Table.class);
        if (!isExist) {
            return null;
        }
        Table t = (Table) c.getAnnotation(Table.class);
        //3、获取注解上的value值
        String tableName = t.value();
        sql.append("select * form ").append(tableName).append(" where 1 = 1 ");
        //4.遍历所有的属性字段
        Field[] fArray = c.getDeclaredFields();
        for (Field field : fArray) {
            //处理每个字段对应的sql
            boolean fExist = field.isAnnotationPresent(Column.class);
            if (!fExist) {
                continue;
            }
            Column column = field.getAnnotation(Column.class);
            //数据库字段名
            String columnName = column.value();
            //5、将user_id 变成 userId
            String[] columns = columnName.split("_");
            StringBuilder columnBuilder = new StringBuilder();
            for (int i = 0; i < columns.length; i++) {
                String s = columns[i];
                columnBuilder.append(s.substring(0, 1).toUpperCase()).append(s.substring(1));
            }
            //6、活动属性值
            String getMethodName = "get" + columnBuilder.toString(); //get方法名
            Method getMethod = c.getMethod(getMethodName);
            Object  fieldValue = getMethod.invoke(object);//类字段值
            //7、拼装sql
            sql.append(" and ").append(columnName).append(" = ").append(fieldValue);
        }
        return sql.toString();
    }
}

测试类

public static void main(String[] args) throws Exception {
        User user = new User(001, 4);
        String query = Query.query(user);
        System.out.println("query = " + query);
    }

运行结果

通过这个小案例实现了通过注解的方式,生成sql语句。

Java 位运算符 &、|、^、~、<<、>>

以前学过有关java的运算符,不过开发了这么久也很少用过这个。现在由于开发需要,所以现在再来回顾整理下有关java的运算符。

主要运算符有以下:与(&)或(|)异或(^)取反(~)左移(<<)右移(>>)

一 与(&) 和 (|)

1、&(按位与)

规则:将两边的数转换为二进制位,然后运算最终值。运算规则即(两个为真才为真)1&1=1 , 1&0=0 , 0&1=0 , 0&0=0。

举例

4&7 这个运算的结果是多少?

3&5 这个运算的结果是多少?

2、|(按位或)

规则 将两边的数转换为二进制位,然后运算最终值。不同的是运算规则(一个为真即为真)1|0 = 1 , 1|1 = 1 , 0|0 = 0 , 0|1 = 1

举例

3|6 这个运算的结果是多少?

5|9 这个运算的结果是多少?

 

二、异或(^) 和 取反(~)

1、异或(^)

规则 异就是不同,其运算规则为1^0 = 1 , 1^1 = 0 , 0^1 = 1 , 0^0 = 0

举例

3^6 这个运算的结果是多少?

5^9 这个运算的结果是多少?

2、取反(~)

规则 取反就是1为0,0为1。

举例

~5 这个运算的结果是多少?

这个其实就是,就是把1变0,0变1。

注意:二进制中,最高位是符号位 1表示负数,0表示正数。

~15 这个运算的结果是多少?

 

三、左移运算(<<) 和 右移运算(>>)

1、左移运算(<<)

规则 左移就是把所有位向左移动几位。

举例

6 << 2 这个运算的结果是多少?

9 << 2 这个运算的结果是多少?

公式 M << n 其实可以这么算 M << n = M * 2^n

2、右移运算(>>)

规则 这个跟左移运算大体是一样的,但是还是有点不同的,不同点在于对于正数和负数补位的时候补的不一样,负数补1,正数补0

举例

12 >> 2 这个运算的结果是多少?

-9 >> 2 这个运算的结果是多少?

从这里就可以看出 正数右移补0,负数右移补1

 

四、负数的进制表示

我们来思考下

1、负的十进制的转二进制

对于十机制的负数,如何用二进制来表示:

思路三步曲1、正数二进制 2、反码 3、反码+1

举例

-1 如何转换成二进制?

说明 -1在计算机里用二进制表达就是全1

2、负的二进制转十进制

上面思路是负的十进制转二进制,那么同样负的二进制转10进制跟上面相反就可以了。

思路三步曲 1、负数的二进制码 2、二进制码-1 3、取反

举例

1111 1101 如何转换成十进制?

 

java提高](18)—静态内部类和非静态内部类

定义 放在一个类的内部的类我们就叫内部类。

自己从开发到现在其实用到内部类主要在两个地方会考虑用内部类:

1、使用静态内部类的单例模式

2、将Json字符串转为Bean实体的时候,也考虑创建内部类

其它比如网上说的通过内部类实现多继承,我还没有用过。这篇博客主要将静态内部类非静态内部类中的成员内部类,至于局部内部类和匿名内部类这边就不阐述了。

一、概念

我们可以简单把内部类当成外部类的一个成员,如果你是static内部类,那么外部类不需要创建对象就可以访问你,

如果你是非静态类部类,那你就属于外部类的对象的,所以一定要先创建外部对象才能来访问你。

1、非静态内部类

成员内部类也是最普通的内部类,它是外围类的一个成员,所以它可以访问外围类的所有成员属性和方法。同样外围类也可以访问内部类的成员属性和方法。

它的主要特点是:

1、成员内部类中不能存在任何static的变量和方法

2、成员内部类是依附于外围类的对象,所以只有先创建了外围类对象才能够创建内部类对象

补充:对于成员内部内并不是完全不能出现static字段的,如果你是使用finalstatic同时修饰一个属性字段,并且这个字段是基本类型或者String

类型的,那么是可以编译通过的。至于原因需要了解class文件,我之前写过一篇相关博客可以参考:【JVM虚拟机】(7)---深入理解Class中-属性集合

2、静态内部类

使用static修饰的内部类我们称之为静态内部类,我们要知道只要是static修饰的类那它一定是内部类,不可能是外部类。

静态内部类与非静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类

的对象,但是静态内部类却没有。没有这个引用就意味着:

1、它的创建是不需要依赖于外围类的对象

2、它不能使用任何外围类的非static成员变量和方法

 

二、示例

有关静态内部类和非静态内部类这里做一个完整的代码演示

1、代码演示

/**
 * 外部类 OutClass
 */
public class OutClass {
    /**
     * 外部类静态属性
     */
    private static String name = "小小";
    /**
     * 外部类非静态属性
     */
    private Integer age = 3;

    /**
     * @Description: 非静态内部类
     */
    private class InnerClass {
        // TODO 非静态内部类不能声明或定义静态成员
        // private static String sex = "女";
        /**
         * 这里定义静态常量是不会报错的哦
         */
        public static final String sex = "女";
        /**
         * 可以定义 普通属性
         */
        private int flag = 0;

        /**
         * 构造函数
         */
        public InnerClass() {
            // 非静态内部类的非静态成员可以访问外部类的非静态变量和静态变量
            System.out.println("非静态类访问外部对象的name" + name);
            System.out.println("外部对象的age " + age);
        }
    }
    /**
     * @Description: 静态内部类
     */
    private static class InnerStaticClass {
        /**
         * 静态内部类可以有静态成员和非静态成员
         */
        private static String sex = "女";
        private int flag = 0;

        public InnerStaticClass() {
            System.out.println("静态类访问外部对象的name" + name);
            //静态类只能访问外部的静态成员,不能访问非静态成员
            //System.out.println("外部对象的age " + age);
        }
    }
    
    public static void main(String[] args) {
        System.out.println("==========非静态内部类调用==========");
        //new一个外部类
        OutClass outClass = new OutClass();
        // 通过外部类的对象new一个非静态的内部类
        OutClass.InnerClass innerClass = outClass.new InnerClass();
        System.out.println("==========静态内部类调用==========");
        //获取静态内部类的静态属性
        String sex = OutClass.InnerStaticClass.sex;
        //获取静态内部类的非静态属性
        OutClass.InnerStaticClass inner = new OutClass.InnerStaticClass();
        System.out.println(inner.flag);

    }
}

2、总结

这里在针对上面代码做一个总结

  1.静态内部类可以有静态成员(方法,属性),而非静态内部类则不能有静态成员(方法,属性)。
  2.静态内部类只能够访问外部类的静态成员,而非静态内部类则可以访问外部类的所有成员(方法,属性)。
  3.实例化一个非静态的内部类的方法:
         a.先生成一个外部类对象实例
         OutClass outClass=new OutClass();
         b.通过外部类的对象实例生成内部类对象
         OutClass.InnerClass inner=outClass.new InnerClass(); 
  4.实例化一个静态内部类的方法:
         a.不依赖于外部类的实例,直接实例化内部类对象
         OutClass.InnerStaticClass inner=new OutClass.InnerStaticClass();
         b.调用内部静态类的方法或静态变量,通过类名直接调用
         OutClass.InnerStaticClass.static_sex

 

补充

1、内部类的作用

我个人觉得主要有以下三点吧

1、内部类能够提供更好的隐蔽性。因为我们的内部类是可以用private和protected修饰的,所以在必要的时候我们可以保证在其它类里是

无法创建当前内部类对象的。就好比我们常用的静态内部类的单例模式。

2、通过内部类可以实现多继承

3、代码可以更加整洁。因为我们在创建实体类的时候,可能类中还包含其它类,如果这个类只会被当前类使用,那我们只需创建一个内部类就可以了。

2、展示一个静态类部类的单例模式

/**
 * 外部类
 */
public class StaticInnerSingleton {
    /**
     * 私有的静态内部类
     */
    private static class SingletonClassInstance{
        private static final StaticInnerSingleton instance = new StaticInnerSingleton();
    }
    /**
     * 获取单例
     */
    public static  StaticInnerSingleton getInstance() {
        return SingletonClassInstance.instance;
    }
}
BigDecimal详解和精度问题

一、背景

在实际开发中,对于 不需要任何准确计算精度的属性可以直接使用float或double,但是如果需要精确计算结果,则必须使用BigDecimal,例如价格、质量。

为什么这么说,主要有两点

1、double计算会有精度丢失问题

2、在除法运算时,BigDecimal提供了丰富的取舍规则。(double虽然可以通过NumberFormat进行四舍五入,但是NumberFormat是线程不安全的)

对于精度问题我们可以看下实际的例子

  public static void main(String[] args) {
        //正常 3.3
        System.out.println("加法结果:"+(1.1+2.2));
        //正常 -7.9
        System.out.println("减法结果:"+(2.2-10.1));
        //正常 2.42
        System.out.println("乘法结果:"+(1.1*2.2));
        //正常 0.44
        System.out.println("除法结果:"+(4.4/10));
    }

实际控制台输出

为什么会这样原因?

在于我们的计算机是二进制的。浮点数没有办法是用二进制进行精确表示。我们的CPU表示浮点数由两个部分组成:指数和尾数,这样的表示方法一般都会

失去一定的精确度,有些浮点数运算也会产生一定的误差。如:2.4的二进制表示并非就是精确的2.4。反而最为接近的二进制表示是 2.3999999999999999。

浮点数的值实际上是由一个特定的数学公式计算得到的。

 

二、BigDecimal构造函数

1、四种构造函数

BigDecimal(int)     //创建一个具有参数所指定整数值的对象。
BigDecimal(double)  //创建一个具有参数所指定双精度值的对象。
BigDecimal(long)    //创建一个具有参数所指定长整数值的对象。
BigDecimal(String)  //创建一个具有参数所指定以字符串表示的数值的对象。

这几个都是常用的构造器,他们返回的对象都是BigDecimal对象。换而言之,将BigDecimal对象转换为其他类型的对象,我们通过以下几种。

toString()          //将BigDecimal对象的数值转换成字符串。
doubleValue()       //将BigDecimal对象中的值以双精度数返回。
floatValue()        //将BigDecimal对象中的值以单精度数返回。
longValue()         //将BigDecimal对象中的值以长整数返回。
intValue()          //将BigDecimal对象中的值以整数返回。

这里需要非常注意BigDecimal(double)的构造函数,也是会存在精度丢失的问题,其它的不会,这里也可以举例说明

public static void main(String[] args) {
        BigDecimal intDecimal = new BigDecimal(10);
        BigDecimal doubleDecimal = new BigDecimal(4.3);
        BigDecimal longDecimal = new BigDecimal(10L);
        BigDecimal stringDecimal = new BigDecimal("4.3");
        System.out.println("intDecimal=" + intDecimal);
        System.out.println("doubleDecimal=" + doubleDecimal);
        System.out.println("longDecimal=" + longDecimal);
        System.out.println("stringDecimal=" + stringDecimal);
    }

控制台实际输出

从图中很明显可以看出,对于double的构造函数是会存在精度丢失的可能的

2、为什么会出现这种情况

这个在new BigDecimal(double)类型的构造函数上的注解有解释说明。

这个构造函数的结果可能有些不可预测。 可以假设在Java中写入new BigDecimal(0.1)创建一个BigDecimal ,它完全等于0.1(非标尺值为1,比例为1),但实际上等于

0.1000000000000000055511151231257827021181583404541015625。 这是因为0.1不能像double (或者作为任何有限长度的二进制分数)精确地表示。

因此,正在被传递给构造的值不是正好等于0.1。

3、如何解决

有两种常用的解决办法。

1、是将double 通过Double.toString(double)先转为String,然后放入BigDecimal的String构造函数中。

2、不通过BigDecimal的构造函数,而是通过它的静态方法BigDecimal.valueOf(double),也同样不会丢失精度。

示例

 public static void main(String[] args) {
        String string = Double.toString(4.3);
        BigDecimal stringBigDecimal = new BigDecimal(string);
        BigDecimal bigDecimal = BigDecimal.valueOf(4.3);
        System.out.println("stringBigDecimal = " + stringBigDecimal);
        System.out.println("bigDecimal = " + bigDecimal);
    }

运行结果

这样也能保证,对与double而言,转BigDecimal不会出现精度丢失的情况。

 

三、常用方法

1、常用方法

示例

public static void main(String[] args) {
        BigDecimal a = new BigDecimal("4.5");
        BigDecimal b = new BigDecimal("1.5");
        BigDecimal c = new BigDecimal("-10.5");

        BigDecimal add_result = a.add(b);
        BigDecimal subtract_result = a.subtract(b);
        BigDecimal multiply_result = a.multiply(b);
        BigDecimal divide_result = a.divide(b);
        BigDecimal remainder_result = a.remainder(b);
        BigDecimal max_result = a.max(b);
        BigDecimal min_result = a.min(b);
        BigDecimal abs_result = c.abs();
        BigDecimal negate_result = a.negate();

        System.out.println("4.5+1.5=" + add_result);
        System.out.println("4.5-1.5=" + subtract_result);
        System.out.println("4.5*1.5=" + multiply_result);
        System.out.println("4.5/1.5=" + divide_result);
        System.out.println("4.5/1.5余数=" + remainder_result);
        System.out.println("4.5和1.5最大数=" + max_result);
        System.out.println("4.5和1.5最小数=" + min_result);
        System.out.println("-10.5的绝对值=" + abs_result);
        System.out.println("4.5的相反数=" + negate_result);
    }

运行结果

4.5+1.5=6.0
4.5-1.5=3.0
4.5*1.5=6.75
4.5/1.5=3
4.5/1.5余数=0.0
4.5和1.5最大数=4.5
4.5和1.5最小数=1.5
-10.5的绝对值=10.5
4.5的相反数=-4.5

这里把除法单独再讲一下,因为除法操作的时候会有除不尽的情况,,比如 3,5/3,这时会报错java.lang.ArithmeticException: Non-terminating decimal expansion;

no exact representable decimal result。所以这里要考虑除不尽的情况下,保留几位小数,取舍规则。(除法如果可能存在除不进,那就用下面方法)

BigDecimal divide(BigDecimal divisor, int scale, int roundingMode) 第一参数表示除数,第二个参数表示小数点后保留位数,第三个参数表示取舍规则。

2、取舍规则

ROUND_UP          //不管保留数字后面是大是小(0除外)都会进1
ROUND_DOWN        //保留设置数字,后面所有直接去除
ROUND_HALF_UP     //常用的四舍五入 
ROUND_HALF_DOWN   //五舍六入
ROUND_CEILING     //向正无穷方向舍入
ROUND_FLOOR       //向负无穷方向舍入
ROUND_HALF_EVEN   //向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,如果保留位数是奇数,使用ROUND_HALF_UP,如果是偶数,使用ROUND_HALF_DOWN
ROUND_UNNECESSARY //计算结果是精确的,不需要舍入模式 

注意 我们最常用的应该是 ROUND_HALF_UP(四舍五入)

上面这样解释还是有点模糊,具体可以看这篇文章,示例非常清楚 BigDecimal的四舍五入的RoundingMode 选择

这里举几个常用的取舍规则

 public static void main(String[] args) {

        BigDecimal a = new BigDecimal("1.15");
        BigDecimal b = new BigDecimal("1");

        //不管保留数字后面是大是小(0除外)都会进1 所以这里输出为1.2
        BigDecimal divide_1 = a.divide(b,1,BigDecimal.ROUND_UP);
        //保留设置数字,后面所有直接去除         所以这里输出为1.1
        BigDecimal divide_2 = a.divide(b,1,BigDecimal.ROUND_DOWN);
        //常用的四舍五入         所以这里输出1.2
        BigDecimal divide_3 = a.divide(b,1,BigDecimal.ROUND_HALF_UP);
        //这个可以理解成五舍六入   所以这里输出1.1
        BigDecimal divide_4 = a.divide(b,1,BigDecimal.ROUND_HALF_DOWN);
        //这里将1.15改成1.16
        BigDecimal c = new BigDecimal("1.16");
        //那么这里就符合六入了 所以输出变为1.2
        BigDecimal divide_5 = c.divide(b,1,BigDecimal.ROUND_HALF_DOWN);
        System.out.println("divide_1 = " + divide_1);
        System.out.println("divide_2 = " + divide_2);
        System.out.println("divide_3 = " + divide_3);
        System.out.println("divide_4 = " + divide_4);
        System.out.println("divide_5 = " + divide_5);

    }

运行结果

divide_1 = 1.2
divide_2 = 1.1
divide_3 = 1.2
divide_4 = 1.1
divide_5 = 1.2

 

四、格式化

由于NumberFormat类的format()方法可以使用BigDecimal对象作为其参数,可以利用BigDecimal对超出16位有效数字的货币值,百分值,以及一般数值进行格式化控制。

以利用BigDecimal对货币和百分比格式化为例。首先,创建BigDecimal对象,进行BigDecimal的算术运算后,分别建立对货币和百分比格式化的引用,最后利用

BigDecimal对象作为format()方法的参数,输出其格式化的货币值和百分比。

示例

    public static void main(String[] args) {
        //建立货币格式化引用
        NumberFormat currency = NumberFormat.getCurrencyInstance();
        //建立百分比格式化引用
        NumberFormat percent = NumberFormat.getPercentInstance();
        //百分比小数点最多3位
        percent.setMaximumFractionDigits(3);
        //取整
        NumberFormat integerInstance = NumberFormat.getIntegerInstance();
        ////金额
        BigDecimal loanAmount = new BigDecimal("188.555");
        ////利率
        BigDecimal interestRate = new BigDecimal("0.018555555");
        //没有指定保留位数的情况下 默认保留2位
        System.out.println("金额: " + currency.format(loanAmount));
        //货币(百分比)格式化   指定默认的取舍规则是四舍五入
        System.out.println("利率: " + percent.format(interestRate));
        //取整还有点不一样 188.555取整为189, 188.51也是189 但是189.5确实188,所以它不是真正意义上的四舍五入
        System.out.println("取整: " + integerInstance.format(loanAmount));
    }

运行结果

金额: ¥188.56
利率: 1.856%
取整: 189

这里有几点在说明下

1、格式化的时候没有指定保留位数的情况下 默认保留2位

2、货币(百分比)格式化 指定默认的取舍规则是四舍五入

3、取整还有点不一样 188.555取整为189, 188.51也是189 但是189.5确实188,所以它不是真正意义上的四舍五入

posted @ 2022-02-25 16:31  hanease  阅读(48)  评论(0编辑  收藏  举报