java总结2024

java篇:

一、设计原则:

  六大设计原则:

    1、单一职责原则:

      定义:即一个类或者模块只完成一个职责

      好处:使用更灵活,效率更高

      例子:例如篮球队,足球队,每个位置的人只需要负责自己位置的职责。

    2、里氏替换原则:

      定义:所有使用父类的地方可以使用子类的对象,子类可以扩展父类的功能,但是不是替换父类的功能,如果需求替换父类的功能,建议多用组合,少用继承

      好处:(1)里氏替换原则就是针对继承而言的,如果继承是为了实现代码的重用,也就是为了共享方法,那么共享的父类的方法就应该保持不变,子类只能通过添加新方法来扩展功能。

         (2)如果继承是为了多态,而多态的前提就是子类覆盖并重新定义父类的方法,我们应该将父类定义为抽象类,并定义抽象方法

    3、迪米特原则:

      定义:一个类对与其耦合或调用类所了解的越少越好

    4、依赖倒置原则:

      定义:下层模块引入上层模块的依赖,改变原有的自上而下的依赖方向

      例子:基于上层提供的N个特征,提供符合特征的底层类,基于接口开发

    5、开闭原则:

      定义:类、方法、模块应该对扩招开发,对修改关闭,即添加一个功能,应该是在已有的代码基础上进行扩招,而不是修改

    6、接口隔离原则:

      定义:建立单一接口,不要建立臃肿庞大的接口,接口尽量细化,,同时接口的方法要尽量少。

      好处:(1)接口要尽量小,不要违反单一职责原则,要适度

         (2)接口要高内聚,提高模块、接口类的处理能力,减少对外交互。

         (3) 定制服务,通过对高质量接口的组装,实现服务的定制化。

二、23种设计模式:(按面试出现频率排序)

  1、单例模式:

    定义:一个对象只能被实例化一次,并提供一个全局访问点

    单例模式五种方式:

     (1)饿汉式:

        使用效率高,线程安全,但是不能延时加载。

        因为还没有调用getInstance的时候,对象已经被实例化了。

        public class Singleton1{

          private Singleton1();

          private static final Singleton1 single = new Singleton1();//此时已经实例化

          public static Singleton1 getInstance(){

            retuen single;

          }

        }

     (2)懒汉式:

        使用效率不高,线程安全,可以延时加载

        因为懒汉式是在调用getInstance时,才实例化对象,但是如果多线程的情况下,当第一个线程抢到锁之后,对single对象进行校验,校验为true,实例化对象,释放锁,由下一个线程获得锁,需要重新进行校验,此刻single已经实例化过了,不在是null了,校验为false,返回single对象。由于在多线程的情况下,每个线程都要获得锁,释放锁,校验是否实例化,降低了使用效率。

        public class Singleton2{

          private Singleton2();

          private static Singleton2 single = null; // 此时对象并没有实例化

          public static synchronized Singleton2 getInstance(){

            if(single == null){

              single = new Singleton2();

            }

          return single;

          }

        }

     (3)双重校验:

        使用效率高,线程安全,可以延时加载

        public class Singleton3{

          private Singleton3();

          private static Singleton3 single = null;

          public static Singleton3 getInstance() {

            if(single == null) {// 1

              synchhronized(Singleton3.class){

                if(single == null) {// 2

                  single = new Singleton2();

                }

              }

            }

            return single;

          }

        }

     (4)静态内部类:

        线程安全,使用效率高,延时加载

          这种的好处,是解决了双重校验代码中的繁杂的if,private 静态内部类对外的getInstance方法返回静态类的instance对象,只有第一次访问的时候,才会被创建,类的初始化本身就是执行类的构造器的<clinit>方法,该方法是由javac编译器生成的,他是由一个类里面所有静态成员的赋值语句,和静态代码块组成的,jvm会保证一个类的clinit方法在多线程的环境下也可以正确的加锁同步,只有一个线程会执行clinit方法,在该线程执行的时候,其他线程进入阻塞等待状态,直到这个线程执行完,其他线程才被唤醒,但不会再进入clinit方法,也就是说一个加载器下,一个类只会被初始化一次。

        public class Singleton4{

          private static class lazyLoader{

            private static final Singleton4 INSTANCE = new Singleton4

          }

          private Singleton(){};

          public static final Singleton4 getInstance(){

            return lazyLoader.INSTANCE;

          }

        }

     (5)枚举类:

        枚举本身是单例的,使用较少。一般用来定义内存字典的较多。

  2、代理模式:

    定义:代理模式为另一个对象提供一个替身或占位符,以控制对这个对象的访问,springAop采用的是动态代理

    解决问题:当我们想要对一个业务进行横切性增强时,例如:增加请求与响应的日志、增加权限校验、增加远程请求对象封装等,可以采用代码模式实现,无需改变原有的类。

  3、适配器模式:

    定义:将一个类的接口,转换为客户期望的另一个接口,适配器让原本不兼容的两个类可以合作无间

  4、模板模式:

    定义:在一个方法中定义一个算法骨架,将一些步骤延迟到子类中,模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中某些步骤

  5、中介者模式:

    定义:使用中介者模式来集中相关对象之间复杂沟通和控制方法

  6、建造者模式/生成器模式:

    定义:使用生成器模式,可以封装一个产品的构造过程,并允许按步骤构造产品。

    优点:将一个复杂的对象的创建过程封装起来,允许对象通过多个步骤来创建,并可以改变步骤。向客户隐藏产品内部表现,产品的实现可以替换,因为客户看到的只是一个抽象接口。

    例子:可以类比购买车模型,可以直接购买成品,如果对内部构造比较感兴趣,可以购买乐高自行组装。

  7、装饰模式:

    定义:动态的将责任附加到对象上,若要扩展功能,装饰者提供了比继承更弹性的替代方案,

    例如:煎饼,化妆等。

  8、门面模式:

    定义:提供了一个统一接口,用来访问子系统中的一群接口,外观定义了一个高层接口,让系统更容易使用。

  9:命令模式:

    定义:将“请求”封装成命令对象,以便使用不同的请求,队列,日志来参数化其他对象。

      上游的命令不关心下游的命令谁去做,下游去做的也不关心上游的命令是谁发出的。上下游解耦。

  10:职责链模式:

    定义:使多个对现象都有机会处理请求,从而避免了请求的发送者和接收者的耦合关系,将这些对象连成一个链条,并沿着这条链传递请求,直到有对象处理它为止。

  11:观察者模式:

    定义:定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它所依赖者都会收到通知并自动更新。

  12:访问者模式:

    定义:表示一个作用于某个对象结构中的各个元素的操作,它使你在不改变各元素的类额前提下定义作用于这些元素的新操作。

三、反射:

  1、什么是反射:

    反射是在运行状态中,对于任意一个类,都可以获取该类的方法和属性;对于任意一个对象,都能调用其方法和属性,这种动态获取信息和动态调用对象方法的功能,称为java语言的反射机制

  2、在什么地方用到反射:

    在jdbc中,利用反射动态加载数据库驱动程序

    开发中比较常见的入参的对象和我们要存储的对象属性基本相同,这种情况下可以使用反射,将对象添加进去,避免了代码中大量的get和set。

  3、如何用反射将一个对象里属性相同的值,赋值给另外一个对象:

    ①使用反射实现,BeanUtil工具

    Public class BeanUtil{

      public static void convert(Object orginObj, Object targetObj) throws Throwable{

        Class orginClazz = orginObj.getClass();

        Class targetClazz = targetObj.getClass();

        Map<String, Field> fieldCache = new HashMap<>();

        Field[] orginFields = orginClazz.getDeclaredFields();

        for(Field field : orginFields) {

          fieldCache.put(field.getName(), field);

        }

        for(Field targetField : targetClazz.getDeclaredFields()) {

          if(!fieldCache.containsKey(targetField.getName())) {

            continue;

          }

          Field orginField = fieldCache.get(targetField.getName());

          orginField.setAccessable(true);

          targetField.setAccessable(true);

          targetField.set(targetObj, targetField.get(OrginObj));

        }

      }

    }

    ②还可以使用mapStruct工具

      步骤:Ⅰ.先引入org.mapStruct依赖

          Ⅱ.@Mapper

            public interface CarMapper{

             CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);

           @Mapping(source = "numberOfSeats", target="seatCount");

           @Mapping(source="type", taarget = "typeDto")

            CarDto carToCarDto(Car car);

            }

          其底层原理依旧是setxxx(getxxx());的方式,可以避免手写。

    ③还可以使用JsonUtil工具,转成String,在通过JsonUtil.toObject(指定的实体类)实现

  4、反射机制和new对象在类加载的过程中有什么区别

    不管是反射还是new对象的形式在类加载的过程中,都需要打开&检查class文件,区别就在于new对象的时候,是在类加载去做的,反射是在运行期做的。

  5、jvm类加载流程和内存结构:

    类加载器ClassLoader(将class文件加载到jvm中)

    加载-->验证-->准备-->解析-->初始化

    new对象和反射都需要解析class文件,class文件中包含类相关的所有相关信息,如:类名,包名,属性,方法等。class对象就是我们获得某一各类型的字节码的引用对象。

  6、反射的方式有几种:

    三种

    1、Class personClazz  = Person.class();

    2、Person person = new Person();

       Class personClazz1 = person.getClass();

    3、Class personClazz2 = class.forName("com.xxx.xxx.Person");

    personClazz == personClazz1;

    personClazz == personClazz2;

    三种方式获得的class实例相同

  6、反射的步骤:

    无论是new对象还是使用反射机制,创建实例对象的步骤,都需要以下三步:

    1、加载class文件

    2、查找入参匹配的构造函数

    3、通过构造函数创建实例对象

    Class personClazz = Class.forName("com.xxx.xxx.Person");

    Constructor constructor = personClazz.getContructor();

    Person person = (Person) constructor.newInstance();

  7、单例模式可以被破坏么,如何破坏?

    通过反射可以在运行期动态加载想要加载的类,通过反射可以破坏单例模式,是一个对象可以多次实例化

    通过getDeclaredConstructor的方式即可

      第一步:通过反射创建对象

        Class singlePersonClazz = Class.forName("com.xxx.Person");

      第二步:获得构造函数

        Constructor constructor = singlePersonClazz.getDeclaredConstructor();// 即可获得私有化构造函数,但无使用权限

        constructor.setAccessable(true);// 可以使用私有构造函数,也可以再次实例化对象。

  8、getDeclaredField和getField有什么区别

    getDeclaredField可以获取一个类里的所有修饰的字段,但无法获取父类的字段

    getField可以获取一个类里所有的public修饰的字段,也可以获取到父类字段,但无法获取到public以外的字段。

  9、什么是双亲委派机制

    执行类加载的时候,从哪个类开始的,也从哪个类结束,比如:某个类从Extension ClassLoader开始加载,Extension ClassLoader委派给父类Bootstrap ClassLoader,若BootStrap ClassLoader依旧没有加载上,则开始向下加载,加载到Extension ClassLoader结束。

 

   10、为什么需要spi破坏双亲委派机制:

     SPI是Service provider Interface的缩写,是java提供的一套用于被第三方开发或者实现的API接口,可以用于模块化,解耦,插件机制等。如果本身是启动类加载器,双亲委派机制,无法加载到下面的层级,通过线程,可以获得当前线程的应用类加载器,应用类加载器可以去获得ClassPath里的相关类,即可见性原则,上层类加载器对下层类加载器是可见的,然而下层类加载起对上层类加载器是不可见的,所有由java团队设计了线程上下文类加载器,如果不破坏双亲委派机制,无法从ClassPath里获取到相应的类

 

四、泛型:

  1、什么是泛型,为什么要用泛型:

    早期java使用Object类型来代表任意类型,但是向下转型有强转的问题,线程也不安全,所以针对List、Set、Map等集合类型,它们对存储的元素是没有任何限制的,假如向List中存储一个Dog类型的对象,但是有人把Cat类型也存储到List中,编译上没有任何语法错误,所以把所有使用该泛型参数的地方都被统一化,保证类型一致,如果未指定具体的类型,默认是Object类型,集合体系中是所有的类型都增加了泛型,泛型也主要用于集合。

  2、什么是泛型类:

    泛型类就是把泛型定义在类上,用户使用该类的时候,才把类型明确下来,用户明确了什么类型,该类就代表什么类型,也不用担心强转的问题和运行时转换异常的问题。

  3、为什么使用泛型方法:

    除了在类上使用泛型,我们可能就要仅仅某个方法上需要使用泛型,外界仅仅关心该方法,不关心其他的属性,如果在整个类上定义泛型,未免小题大做,所以采用泛型方法。

  4、什么是类型擦除:

    泛型是提供给java编译器使用的,它可以作为类型的限制,让编译器在源码级别上挡住非法类型的数据,在java1.5之后,编译器编译完带有泛型的java程序后,生成的class文件里将不再带有泛型的信息,这个过程叫做类型擦除,擦除掉“T”由Object替代,编译器会自动生成一个桥方法,因为擦除掉之后,就会失去了类型的校验,可以用method.briged()来判断一个method是不是桥方法。

  5、类型通配符(不常用)

    List<?>表示类型未知的list,可以匹配任何类型的元素,声明List<?>后,不能向集合中添加元素,因为无法确定集合的元素类型,唯一例外的是可以添加null

  6、泛型的上限和下限

    泛型的上限:

      格式:<? extends ? 类> 对象名称

      意义:只能接收该类型及其子类

 

    泛型的下限:

      格式:<? super ?类> 对象名称

      意义:只能接收该类型及其父类类型

 

五、ArrayList和LinkedList:

  1、ArrayList的底层逻辑:

    创建一个ArrayList就是创建了一个空数组,Object [],初始值的大小是0,存入一个元素,首次扩容至默认值为10,之后按1.5倍向下取整进行扩容。

  2、为什么是按照1.5倍扩容?

    通过源码来看,int newCapacity  = oldCapacity + (oldCapacity >> 1);

    右移一位表示÷2,左移一位表示乘2,

    举例说明:32 = 2^5,BIN = 0100000。即从右向左查,1在下标5的位置,即2^5;右移一位之后,BIN变成0010000也就是2^4 = 16;左移一位BIN变成01000000 1在下标6的位置,也就是2^6 = 64。

  3、ArrayList与LinkedList的区别?

    ①ArrayList的底层是数组,LinkedList的底层是双向链表。

    ②ArrayList增删比较慢,但是查询很快,LinkedList增删很快,查询很慢

    ③ArrayList的内存空间是连续的,LinkedList的内存空间可以是不连续的

    ④两者都不是线程安全的。

   4、为什么ArrayList增删比较慢,查询较快,而LinkedList的增删很快,查询很慢:

      ArrayList的查询比较快是因为ArrayList的内存空间是连续的,CPU内部缓存结构会缓存连续的内存片段,可以大幅降低读取内存的性能开销。而linkedList查询时根据index,如果index在前半段则是从头节点开始向后遍历,如果index在后半段则是从尾节点开始遍历。ArrayList增删的时候,可以有两种方式,一种是指定index插入的,一种是尾插法,无论是那种方法,ArrayList在新增的时候,都会去调用ensurecapacityInternal来校验数组长度,如果长度不够,会进行扩容,以确保已存在的数组有足够的容量来存储这个新元素,旧数组会被使用Arrays.copyOf方法复制到新的数组中去,现有的数组引用指向了新的数组。而linkedList在新增的时候采用的是尾插法,并且无需遍历尾节点,因为在LinkedList的代码中已经保存了尾节点,直接在尾节点后插入就可以了。

  5、如何复制一个ArrayList到另外一个ArrayList中去:

      根据业务场景的不同,可以采用深克隆和浅克隆,或者ArrayList的构造方法,或者Collection的copy方法

  6、既然说ArrayList和LinkedList都是线程不安全的,为什么还要使用呢

       因为在使用ArrayList的场景尽量使用多查询少增删的地方,不会太涉及增删操作。而如果碰到频繁的增删操作的时候采用linkedList,如果需要使用线程安全的可以使用vector和CopyOrWriteArrayList。

  7、ArrayList和LinkedList如何相互转换

    采用构造方法转换。或者list的add方法

 

六、HashMap:

  1、HashMap的数据结构

    数组加链表加红黑树

    其中链表为单项链表,红黑树则为双向的,当链表长度超过8时,链表转换为红黑树

  2、HashMap的底层逻辑及工作原理

    hashMap的底层原理是hash数组➕单项链表实现的,数组中每个元素都是链表,由node的内部类Map.entry接口实现,hashMap通过put和get方法存储和获取。

    ①存储对象时,将K / V键值对传给put()方法,调用hash(K)方法计算K的hash值,然后结合数组长度,计算数组下标,

    ②根据元素个数调整数组大小,当元素的个数大于capacity*loadFactor时,会进行resize的二倍扩容。

    ③Ⅰ、如果K的hash值在hashMap中不存在则执行插入,若存在,则发生碰撞。

     Ⅱ、如果K的hash值相同,且两者equals返回为true,则执行更新操作。

     Ⅲ、如果K的hash值相同,且两者的equals的返回为false,则在尾部插入数据。

    获取对象的时候,将K传给get()方法:

      ①调用hash(K)方法,计算K的hash值,从而获取该键值的所在链表的数组下标,

      ②顺序遍历列表,equals()方法,查找相同Node链表中K值对应的V值。

  3、hash如何实现的

    通过HashCode()的高16位异或低16位实现的,可以有效的节省系统开销,规避碰撞,高16位异或低16位的的也叫扰动函数,高16位的值右移到低16位,原低16位的值去掉,现在低16位的值变成原高16位的值,现在的高16位的值补0。然后做异或操作,当前是1,现在还是1的变成0,原先是0的现在还是0的依旧是0,原先是1,现在是0的变成1,原先是0现在是1的也变成1

  4、两个对象hashCode相同会怎么样?

    hashCode相同,并不代表两个对象相等,用equals方法进行比较,所以两个对象所在的数组下标相同,就会发生碰撞,因为HashMap是使用的链表存储对象,所以这个node会存储到链表中。

  5、hashMap的table容量如何确定

    table数组的大小是用capacity这个参数决定的,默认是16,也可以构造时传入,最大值是1<<30.10亿多。

    装载因子LoadFactor,主要用来确认table数组是否需要动态扩展的,默认值是0.75。比如说table的大小是16,默认值是0.75,阈值就是12,当table的实际大小超过12时,就需要进行扩容。

    扩容时调用resize()方法,将table的长度变为原来的两倍。

  6、hashMpa里put和get的过程。

    hashMap的put的过程,首先将K / V键值传给put方法,put方法调用hash(K),计算K的hash值,结合数组长度,计算数组下标。

    根据元素个数,调整数组大小,当元素个数大于capacity*loadfactor时,进行resize的二倍扩容。

    如果K的hash值在hashMap中不存在,则直接可以在链表的尾部插入数据,或者红黑树中。

    如果存在,则调用equals方法,如果返回true,则进行更新操作。如果返回为false,则在链表后插入数据或者在红黑中添加数据。

    get的过程是将K传给get()方法,调用hash(K)方法,计算K的hash值,从而获得该键值的所在链表的数组下标。顺序遍历列表,用equals()方法,查找相同的Node链表中K值对应的V值。

  7、可以使用二叉树么,为什么使用红黑树,不用二叉树:

    可以使用二叉树,不过二叉树在特殊情况下可能会变成一个线性结构,和链表一样了,这样会导致遍历查询变得非常慢

  8、什么是红黑树

    ①只有红色和黑色两种节点

    ②所有的叶子节点都是黑色的

    ③根节点也是黑色的

    ④红色节点的左右子节点都是黑色的

    ⑤除去红色的节点,任意一个节点到其中一个节点的距离都是相同的

  9、hashMap、LinkedHashMap、TreeMap有什么区别及使用场景

    hashMap是比较常用的,hashMap只允许一条记录的建为null,允许多条记录的值为null,hashMap不支持线程同步,即在任一时刻,有多个线程在同时写HashMap可能会导致数据不一致,如果想要同步的话,可以使用Collections的synchronized方法使HashMap具备同步能力。或者使用ConcurrentHashMap,在map中插入,删除,定位数据的时候,用hashMap的比较多。

    LinkedHashMap会保存存储的顺序,遍历LinkedHashMap的时候,也是按照存储的顺序进行输出的。LinkedHashMap遍历的速度和容量无关,只和数据相关。而hashMap的遍历速率和他的容量相关。输出的顺序和输入的顺序一致的时候使用LinkedHashMap

    TreeMap实现sortMap接口,可以把保存的数据按照键值排序,默认升序,遍历TreeMap的时候,数据已经是排序后的,也可以指定排序的比较器。

  10、hashMap和hashTable有什么区别

    hashMap是线程不安全的,hashTable是线程安全的,hashMap的效率更高,。

    hashMap最多只允许一条记录的键为null,可以有许多值为null。hashTable则不允许有空键值对。

    hashMap默认初始化数组大小是16,hashTable的默认初始化大小是11,hashMap是二倍扩容,hashTable是二倍加1扩容

    hashMap需要重新计算hash值,hashTable则直接使用对象的hashCode。

  11、HashMap和ConcurrentHashMap有什么区别

    concurrentHashMap是在hashMap的基础上加了锁。原理是并无太大区别,hashMap可以空键值对,concurrenthashMap不允许有空键值对。

  12、concurrentHashMap锁机制怎么理解

    concurrentHashMap采用的是Node+CAS+synchronized来保证并发安全的,直接用table数组保存键值对,当hashEntry的长度超过阈值,链表转为红黑树,底层变更为数组+链表+红黑树。

  13、hashmap什么时候采用红黑树,什么时候不用红黑树

    当hashMap中的元素超过8个的时候,采用红黑树,当元素为6的时候退化为链表,其实6和8都是大量的经验结果,是经过数学计算的,当元素小于8的时候,使用红黑树的话,元素比较少,新增的效率比较慢,而链表结构是可以保障查询性能的。如果一个hashMap不停的存储和删除的话,链表个数始终在8徘徊,就会导致链表和红黑树之间的不断切换,效率也会很低,所以有一个中间值7,防止链表和红黑树之间的频繁转换。

  14、红黑树是如何做到自平衡的,如何理解红黑树的左旋和右旋

    当红黑树插入或者删除节点的时候,可能会造成不平衡,这个时候红黑树就可以通过左旋和右旋以及变色来维持自平衡,假设有两个子节点,分别是x和y,y是x的右子节点,这个时候可以进行左旋操作,通过左旋使x和y互换,变成x是y的左子节点,这个过程就是左旋,同理,假设最开始的时候,y是x的左子节点,通过右旋,是x和y互换,变成x是y的右子节点,这个过程叫做右旋。

  15、jdk1.7采用的是头插法,jdk1.8采用的是尾插法,为什么头插法会导致死循环,尾插法不会?

      因为当有两个线程的时候,分别是线程A和线程B,两个线程同时对table进行数据迁移,两个线程都读取了待迁移元素A,和下一个元素B,线程A正常执行元素迁移,线程B由于某种原因,还未执行数据迁移操作,当线程A执行完之后,会将当前的table数组,替换为新的Table数组,等线程B开始执行数据迁移操作的时候,当前使用的table数组就不再是旧的Table数组,而是线程A执行后的新的table数组,由于原本的关系是A的next是B,现在变成B的next是A,会形成循环链表,迁移操作将无限循环。

框架篇:

七、Mybatis

   一、采用JDBC方式访问数据库

    使用JDBC的5个步骤

       ①注册驱动和数据库信息(jdbcDriver)

       ②获得Connection,并使用他打开statement对象

       ③通过statement对象执行sql语句,并获得结果对象的ResultSet

       ④通过代码将ResultSet对象转化位POJO对象

       ⑤关闭数据库资源

    优点:

      ①我们只需要会调用JDBC接口中的方法即可,使用简单

      ②使用同一套java代码,进行少量的修改就可以访问其他JDBC支持的数据库

    缺点:

      ①代码量很大,编码麻烦

      ②需要我们对异常进行正确的捕获并关闭链接。

    采用Hibernate访问数据库

      使用Hibernate的3个步骤

        ①引入Hibernate的Maven依赖

        ②在hibernate.cfg.xml配置文件中配置数据库数据源

        ③编写User.hbm.xml配置文件,配置User和tb_user表的映射关系

      优点:

        ①将映射规则分离到xml/注解中,减少代码耦合度。

        ②无需管理数据库链接,只需配置相应的xml配置文件

        ③会话只需要操作Session对象即可,关闭资源也只需要关闭Session即可。

      缺点:

        ①全表映射不便利,更新是需要发送所有字段

        ②无法根据不同的条件组装不同的sql

        ③对于多表关联和复杂的sql查询支持较差

        ④HQL性能较差,无法优化sql

 

posted @ 2024-07-11 22:11  心声有否偏差  阅读(1)  评论(0编辑  收藏  举报