学Java几个月了,一直在往下学、复习、往下学…感觉还是需要复习复习。以下知识点均为自己复习总结,可能存在错误。(太过基础的不列举了)
一、语言基础
switch结构中表达式可以为byte,short,int,char,枚举类型(JDK5新增),String类型(JDK7新增)
for(;;)表示for循环的死循环 while循环的死循环为while(true)
数组声明默认初始化整数型:0;浮点型:0.0;boolean型:false;char型:unicode编码的0;引用数据类型:null
二维数据默认初始化:方式一:int [][]a=new int[3][4];外层为地址,内层与一维数组判断相同。方式二int [][]a=new int[3][] 外层元素初始化:null,无内层元素。
复制array1数组为array2:不可以array2=array1,这种做法只是array2指向了array1。复制做法是new一个新的array2,之后挨个赋值。
方法的重载:要在同一类,方法名要一样,参数类型不一样。参数类型的顺序也算。比如 test(int a, char b)和 test(char a, int b)也构成方法的重载。重载与返回值无关,根权限修饰符无关。要看形参类型的顺序,而不是看形参的顺序如:test(int a, char b)和test(int b, char a)不构成重载。
另外如果调用test(1,2) 当有test(int a,int b),test(double a,double b)还有一些其他的test重载方法时候会执行第一个,当有test(double a,double b)和一些其他的test重载方法时候会执行test(double a,double b)其中关乎到了自动向上转型。比如System.out.println()除了有println(基本数据类型)方法,还有println(Object),但是我们还可以println(我们自己定义的类)。
可变个数形参:是JDK5.0之后新加入的,声明格式 …变量名。test(int i)方法和test(int … i)也构成重载,但是当二者内的形参类型不同时 不构成重载。可变个数的形参要使用的话,和数组操作一样。
方法形参的传递都为值传递,当传递进方法的形参为基本数据类型时,是把其副本传入方法,为不影响原本的值。而如果传入的是引用数据类型,在方法里修改该引用数据类型,会影响原本的引用数据类型的对象。因为传入的是对象的地址值,而实际上指向的还是这个对象。
一个比较奇葩的面试题(测试使用JDK1.8)
int[] a={1,2,3};
char[] b={'a','b'};
System.out.println(a);
System.out.println(b);
输出结果
[I@1b6d3586
ab
原因:第二个println走到了重载的方法里。
权限修饰符的范围:public 工程内,protected 不同包的子类+同一包内,缺省 同一包内,private 类内。可以用来修饰属性、方法、构造器、内部类。单纯的类只能用public和缺省。
没有显示定义构造器,则系统默认提供空参构造。但一旦显示定义有参构造,则系统不再提供无参构造。一个类中至少有一个构造器。
属性赋值先后顺序:默认初始化->(显示初始化或代码块赋值,二者看具体的先后顺序)->构造器初始化->set赋值。
JavaBean:类是公共的;有一个无参的公共构造;有属性且有相应get、set方法。
构造器中用this()调用该类其他构造器时,this调用的构造器必须在第一行(意味着一个构造器内最多只能用一个this调用其他构造器,如想多掉可以在this调用的构造器中再用this调用其他构造器)
子类继承父类,获得了父类所有的属性和方法。包括私有化的属性和方法,只不过由于封装而无法直接调用。封装解决的是能不能看到,而继承是能不能得到。
Java支持类的单继承、类的多重继承、接口的多继承。
子类重写的方法的方法名和形参列表要与父类一致。子类重写的方法的权限修饰符要不小于父类的。(注:子类不能重写父类private的方法)。父类返回void,子类必须也是void。父类返回A类型,子类返回A类型或其子类。父类返回基本数据类型,子类必须返回相同的基本数据类型(必须一样,不能上下转型)。子类抛出异常不大于父类抛出的异常。子类和父类中同名同参的方法要么都是非static(重写),要么都是static的(不是重写)。
super可以让子类调用父类的中被重写的方法。在类的构造器中super(参数)可以显示调用父类的构造器,但必须在首行。super和this调用其他构造器只能二选一。在构造器没有显式声明空参构造器,自动调用super()。
类的多个构造器中,至少有一个类的构造器使用了super(形参)。
子类实例化的全过程:从结果上看:创建子类对象以后,堆空间中就会加载所有父类中声明的属性。从过程上看:通过子类构造器创建子类对象时,一定会直接或间接调用其父类的构造器,知道调用到java.lang.Object类中空参的构造器为止。正因为加载过所有的父类的结构,所以才看到内存中有父类的结构,子类对象才可以调用。注意:虽然调用了父类的构造器,但是自始至终那个都只创建了一个子类对象,而未创建父类对象。
多态:父类引用子类对象 父类 名=new 子类 只能适合左面父类的方法,但运行结果看的是子类的结果。 多态的要求:1.继承 2.子类重写父类 3.父类引用子类对象。编译看左边,运行看右边。(多态的好处:可以减少重载方法的设计,设计方法时可以直接放父类,而不用放很多的子类)。多态不适用于属性,属性仍还是父类的属性,属性编译看左边,结果看左边。多态的使用时,父类的方法被称为虚拟方法,虚拟方法的调用也可以称为动态绑定。多态的使用是运行时行为。
多态性之后,内存中实际加载了子类的方法和属性,只不是声明为父类类型,而无法调用子类独有的属性和方法。想要调用子类独有的属性方法,向下强制转型 子类 名=(子类) 父类名。为了知道是否是子父类关系:a instanceof A:判断a对象是否是类A的实例。是返回true,不是返回false。使用场景:为了避免类转换时候classcastexception. 如果 a instanceof A返回true,如果B是A的父类,则a instanceof B也返回true。
this.属性得到的并不一定就是本类的,还可能是父类的,只不过是先从子类找。
Object类是所有类的父类,其中没有属性只有方法。其中的clone()方法返回一个新的Object对象。equals()。finalized()方法是在垃圾回收之前要调用的方法。getClass()返回类对象。
equals和==的区别:1.==可以使用在基本数据类型中也可以在引用数据类型中。如果在基本数据类型中,比较两个变量保存的数值是否相等。(类型不一定相等,但是其他类型不能和boolean类型比较)。如果比较的是引用数据类型,则比较的是地址值是否相等(是否为同一个对象)。2.equals方法是一个方法,适用于引用数据类型,如果equals方法没被重写即调用的是Object的equals方法则和==一样,如果重写则看重写的方法如何比较(如String,Date,File,包装类等重写了方法,比较的是值而非地址)。
String的equals方法:先判断是否为一个对象;之后判断instanceof,之后判断两个的底层char数组的长度,之后挨个比较是否一样。
如果字符串String s=“a” String q=“a”;a==b返回true。因为这样定义的字符串放在字符串常量池中。
System.out.print(a)方法其实是调用a的类中的tostring方法。
基本数据类型、包装类、String的转化:
1.基本数据类型到String:第一种用加号拼接;第二种String类的valueOf(基本数据类型)
2.String到基本数据类型:第一种用相应包装类的parseXxx(String);第二种通过包装类构造器+自动拆箱
3.基本数据类型到包装类:第一种用相应包装类的构造器;第二种自动装箱
4.包装类到基本数据类型:第一种用包装类的方法xxxValue方法;第二种自动拆箱
5.包装类到String:第一种包装类对象的toString方法;第二种调用包装类的toString(形参)方法
6.String到包装类:包装类的构造器(字符串)。要注意字符串包含的都是什么类型,但是boolean经过优化如果进入构造器的字符串是true则包装类是true否则为false。
三元运算符要求:左右的类型一样,涉及到转型。
例:Object o1=true?new Integer(1):new Double(2.0);
当true时候输出的是1.0,因为发生了int到double 的转型
而如果用if else来写上面true时输出的就是1。
Integer i=new Integer(1);Integer j=new Integer(1); i==j输出false
Integer i=1;Integer j=1; i==j输出true;
Integer i=128;Integer j=128; i==j输出fales;
原因:有个Integer类中有个IntegerCache结构定义了Integer数组来缓存-128到+127的整数来加快装箱速率,当自动装箱时在这个范围则使用这个数组的数而不用new,使用后数组的数不会被销毁。
static修饰的成员变量是静态变量。静态(类)变量可以由所有实例共享。随着类的加载而加载的,因此可以直接用类.静态变量使用。存放在方法区的静态域中。
static修饰的方法是静态方法,随着类的加载而加载。也可以直接用类.方法调用。静态方法中只能调用静态变量而不能调用实例变量,且不能用this和super。
静态代码块随着类的加载而执行只能执行一次,非静态代码块随着对象的创建而执行可以多次执行。非静态代码块可以在创建对象时对对象的属性等进行初始化。如果一个类中定义了多个静态代码块,则按照声明的先后顺序进行执行。静态代码块执行一直在非静态代码块之前执行。如果一个类中定义了多个非静态代码块,也按照声明的先后顺序进行执行。静态代码块只能调用静态的属性方法,不能调用非静态的。
final修饰的类不能被继承,比如String类、System类、StringBuffer类。final修饰的方法不能被重写,如Object类中的getClass。final修饰的属性不能被修改(常量),类中的常量的赋值只能显式声明赋值,先声明之后代码块赋值,先声明之后构造器赋值。具体该如何赋值需要看具体情况。
例子:是正确的。o不能变但是o的属性可以变。
public void add(final Other o){
o.i++
}
class Other{
public int i;
}
native关键字:调用本地的C/C++代码。
abstract可以用来修饰类和方法。抽象类不能实例化,但是仍有构造器给子类实例化时调用。抽象方法只有声明没有方法体。包含抽象方法的类一定是抽象类,但抽象类不一定包含抽象方法。子类必须重写所有父类的抽象方法否则仍未抽象类。abstract不能修饰属性、构造器、私有方法、静态方法、final的方法
接口(interface)中可以定义:全局常量public static final(书写时可以省略不写但默认还在) 、抽象方法 public abstract(JDK1.8开始还能定义静态方法、默认方法)。接口没有构造器。类用implement来实现接口,如果实现类没有实现了全部方法则为抽象类。接口中定义的静态方法只能由接口调,实现类调不了。默认方法可以通过实现类的对象调用,如果实现类重写了默认方法,调用的还是重写的方法。如果子类或实现类继承的父类和实现的接口中声明了同名同参的方法,在子类没有重写此方法的情况下,默认调用父类中的同名同参方法,如果想指定调用父类的可以super.方法,如果想指定调用接口的可以接口.super.方法。如果实现类继承了多个接口,且多个接口定义了同名同参的方法,在实现类没重写时,报错,必须实现类中重写。(默认方法:(public) default (返回值) 方法名(形参){方法体})
一个类有继承有实现,先写继承后写实现。接口之间可以继承而且可以多继承。
如果一个类继承A类,实现B接口,A类中定义了int x=1,B接口中定义了int x=0,如果A要调用x,不能直接调用x(直接调用编译不通过),因为父类和接口是同级的关系。要想调用父类的x要super.x;要想调用接口的x要B.x,因为接口中定义的是全局常量。
注意:接口中定义的变量都是全局常量!!!!!
内部类:成员内部类(定义在类内)和局部内部类(定义在方法内、代码块内、构造器内)
成员内部类一方面作为外部类的成员另一方面作为一个类。作为外部类的成员可以调用外部类的成员,可以被static修饰,可以被4种不同修饰符修饰。作为一个类可以定义构造器、方法、属性,可以被final修饰,可以被abstract修饰。如何实例化:(静态)Person.Dog dog=new Person.Dog();(非静态)Person person=new Person;Dog dog=person.new Dog()。调用:当没有同名的可以直接调用外部类的,如果有同名直接用名是方法的形参名,this.名调用的是内部类 的,外部类(类名而非实例化类名).this.名调用的外部类的。
如果局部内部类的方法中要调用声明局部内部类的方法的局部变量,那么该变量必须为final。
常见的异常:编译时(受检)异常:IOException(FileNotFoundException),ClassNotFoundException。运行时(非受检)异常:NullPointerException;ArrayIndexOfBoundsException;ClassCastException;NumberFormatException;InputMismatchException;ArithmeticException。
处理异常方法:try-catch-finally和throws+异常类型
当catch到一个异常就走到finally,不会执行下面代码。当catch的异常类型没有继承关系,则上下无所谓,有继承,大的放到下面。try中声明的出去了try就不能调用。catch的处理方法 1.String getMessage() 2.printStackTrace()
finally语句一定是会被执行的。即使catch中出现异常,try中出现return,catch中有return等情况。
return和finally的执行顺序,try中的return看是否出错能不能走到能走到就先执行finally再try的return,如果出错不能走到则走到catch中先输出finally再输出catch的return。如果finally中有return则走到finally时自动走finally的return。
子类重写的方法抛出的异常类型不能大于父类被重写的方法抛出的异常
异常对象的产生:1.系统自动生成的异常对象 2.手动生成一个异常对象,并抛出throw。throw new 异常名(“可以写文字”)
二、多线程
程序是为了完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。–生命周期。进程是资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
线程:进程可以进一步细化为线程,是一个程序内部的一条执行路径。若一个进程同一时间并行执行多个线程,就是支持多线程的。线程是调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。一个进程的多个线程享受相同的内存单元/内存地址空间–它们从同一堆中分配对象,可以访问相同的变量和对象。这使得线程间通信更简便、高效。但多个线程共享系统资源可能带来安全隐患。
一个java应用程序至少有三个线程。1.main()主线程 2.gc()垃圾回收线程 3.异常处理线程.
并行与并发:并行–多个CPU同时执行多个任务。并发:一个CPU(采用时间片)同时执行多个任务。
多线程优点:1.改善用户体验 2.提高cpu利用率 3.改善程序结构 将一个长的单线程变为简单的多线程
多线程创建方式一:1.继承Thread类 2.重写run()方法 (将此线程执行的操作放到run方法里) 3.创建继承Thread类的子类的实例对象 4.调用此对象的start()方法。start有两个作用,1.启动线程,2.调用当前线程的run()。不能通过run()启动线程,如果调用run()则只是调用一个方法,但是线程没有启动。一个start()方法一个线程只能用一次。
Thread类的常用方法:void start() 启动线程并执行对象的run()方法。run()线程在被调度时执行的操作。String getName()返回线程的名字(线程明明还能创建实现类时用构造器赋名)。void setName(String name)设置线程的名称。static Thread currentThread()返回当前执行代码的线程线程。static void yield()线程让步,多个线程在重新争夺CPU,只不过是优先级高的可能性高。join()强制执行该线程,其他线程进入阻塞状态,直到该线程执行完毕。stop()强制停止线程的生命周期,但不推荐用。sleep(long ms)线程休眠ms毫秒(注意这里有异常,如果在Thread类中,只能trycatch,因为不能抛出比父类更大的异常)。boolean isAlive() 判断线程是否存活。
Java的调度方法:对同优先级线程组成先进先出队列,使用时间片策略。对高优先级,使用优先调度的抢占式策略。
线程的优先级1(MIN_Priority)~10(MAX_Priority),默认为5(NORM_Priority)。getPriority()获得线程的优先级。setPriority(int newPriority)设置优先级。优先级低只不过是概率低,并非一定在高优先级后执行。
多线程创建方式二:实现Runnable接口。1.创建一个实现了Runnable接口的实现类。2.实现类实现Runnable接口的抽象方法run().3.创建实现类的对象。4.将此对象作为参数传递到Thread类的构造器中,创建Th’read类的对象。5.通过Thread类的对象调用start()方法(Thread类的对象调用了run(),run调用了构造器中声明对象的run)。
两种创建方式的对比:优先使用方式二。原因:1.实现的方法没有类的单继承的局限性。2.实现的方式更适合来处理多个线程有共享数据的情况。二者联系:都实现了Runnable接口。二者都需要重写run()。
线程的生命周期:五种:(NEW)新建、(WAITING)就绪、(RUNNABLE)运行、(BLOCKED)阻塞、(TIMEINATED)死亡。调用start后没被CPU分配资源就进入就绪状态。完成全部工作或被提前强制中止或出现异常线程就死亡。

线程安全问题如何解决:方式1同步代码块。方式2同步方法。synchronized。JDK5.0之后新加Lock锁。
方式1同步代码块:synchronized(锁){代码块} 注意:任何一个类的对象都能成为锁(即使不是要监视的对象也行)(可以用类.class来充当),但必须保证锁的唯一性。
方式2同步方法。方法的修饰符和返回值之间加入synchronized关键字。同步方法内的同步监视器默认为this,静态方法的同步监视器默认为当前类的本身(class)。
用同步代码块解决继承方式和实现方式的线程安全一定要注意同步锁是否唯一。用同步方法解决实现方式的线程安全无所谓,用来解决继承方式的线程安全一定要注意,同步方法内的同步监视器默认为this,而继承方法创建多线程要实例化多个对象,所以如果想用同步方法那么需要保证方法的唯一性(static)
死锁:不同线程分别占用对方需要的同步资源不放弃,都在等对方放弃自己需要的同步资源。出现死锁后不会有异常,但所有线程进入阻塞状态。产生死锁的四个条件:1.互斥条件:一个资源每次只能被一个进程使用(synchronized)2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放3.不剥夺条件:进程已经获得的资源,在未使用完前,不能强行剥夺4.循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。当死锁发生时,只需想办法破除一个条件或多个条件就可以避免。
Lock锁。JDK5.0新加的方法,本身是接口,常用的实现类ReentrantLock。两个构造器,如果用有参构造设置为true,则为顺序同步锁,线程按到的顺序来执行先进先出,无参构造默认为false。实例化Reen,调用实例对象的lock()方法开启,unlock()解锁。
synchronized和lock的异同:相同的:都能解决线程安全的问题。synchronized是在执行完相应的同步代码后自动的释放锁。lock要手动的启动同步、释放锁。优先使用顺序:建议:lock–>同步代码块–>同步方法。
线程通信:wait()当前线程进入阻塞会释放锁,notify()唤醒被wait()的优先级高的被wait的线程,notifyall()唤醒全部。注意:这些方法是用在synchronized的方法的。另外,三种方法的调用者是同步代码块/方法的同步监视器,否则会出现异常。这三个方法是定义在object中的。
sleep和wait方法的异同:执行了都会使线程进入阻塞状态。二者声明的位置不同,sleep在Thread类中,wait在object中。sleep可以在任意时候调用,wait必须要在同步代码块/方法中。wait会释放锁,sleep不会释放锁。
生产者消费者模式:加缓冲区,涉及线程的通信、同步。
多线程创建方式三:实现Callable接口(JDK5.0新增)。1.类继承Callable接口 2.重写call方法 (call方法可以返回Object,而run不能) 3.创建实现的类的实例对象 4.将此对象作为参数传递到FutureTask类的构造器中,创建FutureTas类的对象。5.将FutureTas类的对象作为参数丢到Thread类的构造器中实例化Thread类,调用start方法。(如有需要可以调用该FutureTas类实例对象的get()方法,该方法返回值为call方法的返回值。)如何理解实现Callable接口比实现Runnable接口强大:1. call有返回值 2.call可以抛出异常被外面的操作捕获,获得异常信息 3.Callable支持泛型
线程池好处:不用创建线程提高反应速度。降低资源消耗,不用每次创建线程。便于管理线程,比如核心池的大小,最大线程数,线程存活时间等。
多线程创建方式四:线程池。1.创建指定线程数的线程池 (new Executors.选线程池 )2.执行指定线程的操作。execute(Runnable类实例),submit(Callable类实例) 3.关闭线程池 线程池.shutdown。(如果想设置线程池参数,需要将线程池强转为ThreadPool)。
三、String相关类
String类,是final的,是不可变的字符序列。String 对象的字符内容是存储在一个final的字符数组valuep[]中。实现了Serializable接口,可序列化。实现了Comparable接口。
如果String s=“xxx”,这样创建,则存在字符串常量池中,多个一样的地址一样。构造器中的String默认是字符串常量池的创建方法。
String s=new String(“abc”)方法创建了几个对象。–两个,一个是堆中new的,另一个是常量池中的char数组。如果常量池中已有则只创建堆中一个。
字符串拼接:只有当参与拼接的都是字面量形式(如"ab"+“cd”),才会在常量池中进行,否则都是在堆中(如"ab"+s1)。如果在堆中的String调用intern方法得到的String,则得到的String是存在常量池中的。此外 如果一个String前有final修饰,则编码时候会替换为常量池中(final本就存在常量池中)。
字符串常用方法:int length();char charAt(int index);boolean isEmpty();String toLowrCase();String toUpperCase();String trim()返回字符串副本,忽略前导空白和尾部空白;boolean equals(Object obj);boolean equalsIgnoreCase(String another String)忽略大小写;String concat(String str)字符串拼接;int compareTo(String anotherString);String substring(int beginIndex)返回字符串,从索引到末尾;String substring(int begin int end)新字符串[begin,end)。
不太常用的:boolean endsWith(String),boolean startsWith(String)以指定前缀后缀?;boolean startsWith(String ,int)从指定索引开始以String为前缀;boolean contains(CharSequence)是否包含指定char值序列;int indexof(String);int indexof(String , int fromIndex) 子串第一次索引;lastIndexof(String);int lastindexof(String , int fromIndex) ;replace一大堆重载方法;boolean match(正则表达式);String split(String)根据给定字符串拆分;String split(String,int limit)最多拆分limit个,多了的放到最后一个。
char[]与String转换:String.toCharArray[],new String(char [])
byte[]与String转换:String(byte[]);String(byte[],int offset,int length)从byte[]offset开始length个; String.getBytes();String.getBytes(String charsetName)指定字符集的byte[]。
String是JDK1.0就有的。StringBuilder(JDK5.0新增)和StringBuffer(JDK1.0就有)。StringBuilder线程不安全,但效率高。StringBuffer线程安全但效率低。三者底层都是char数组,但String的char数组为final的。
StringBuilder/StringBuffer的底层分析:StringBuilder s=new StringBuilder();底层是创建了长度为16的char数组。当调用s.append(“a”);value[0]=‘a’。StringBuilder s=new StringBuilder(“abc”);底层是创建了长度为16+abc长度的char数组,之后把abc append到char数组中。(每次造了都空出16个容量)。当调用的length()实际上被重写了返回的是实际的字符数量。扩容问题:添加先判断要添加的字符穿是否为空,不为空继续添加。当需要扩容时,新容器大小为旧容器<<2+2,同时赋值原始字符(存在一些特殊情况)。
二者常用方法:append(String);delete(int start,int end);replace(int start,int end,String);insert(int offset, String);reverse();indexOf(String);substring(int start,int end);int length();charAt(int n);setCharAt(int index,char c)。
StringBuilder效率最高,其次StringBuffer,最差的是String。
四、时间相关类
JDK8之前:
时间戳:System类的currentTimeMillis()。返回型long。
java.util.Date类,构造器Date()可以获取本地时间。构造器Date(long 时间戳)。toString()显示当前时间。getTime()获取当前的时间戳(如果想java.util.Date换为java.sql.Date,需要先java.util.Date进行getTime(),得到的丢到java.sql.Date构造器)。
java.SimpleDateFormat类,用于字符串格式的转化。日期转字符串:String s =SimpleDateFormat(实例化).format(new Date())。字符串转日期:Date d=SimpleDateFormat(实例化).parse(String)。其中SimpleDateFormat类构造器可以指定特定方式。
java.util.Calendar是个抽象类。实例化:方式1:创建子类对象GregorianCalendar 方式2:调用其静态方法getInstance()(实际创建的还是子类对象)。常用方法:get(Calendar.选择要获取的信息),set(Calendar.选择要修改当前对象的信息),add(Calendar.选择要添加当前对象的信息/天数),getTime()日历变日期,setTime(new Date())日期变日历对象。注意1月为0,周日为1.
JDK8中新加的日期类
localDate,localDateTime,localTime三个类。构造都用localTime.now()方法。此外localDateTime.of(指定年月日时间)。getXxx()获得相关属性如getMonth()。localDateTime还有withHour,plusMonths,minusDays()方法等.
Instant类:获取本初子午线对应的标准时间Instant instant = Instant .now();添加时间偏移量OffsetDateTime offsetDateTime = instant . atOffset (ZoneOffset . ofHours(8));toEpochMilli()获取时间戳;通过时间戳获得实例Instant instant1 = Instant.ofEpochMilli(1550475314878L);
DateTimeFormate:格式化或解析日期、时间。
五、其他类
Comparable接口实现规则:当前对象大于形参对象返回正整数,小于返回负整数,等于返回0.
System类:1. currentTimeMillis()返回当前时间戳 2.exit() 0正常退出,非0异常退出。3.gc()请求执行垃圾回收,但是否回收取决于垃圾回收算法和系统执行情况。4.getProperty(String)获得一些属性。
BigInteger表示不可变的任意精度的整数,可以比long更大。BigDecimal类用于商业计算,不可变、任意精度的有符号十进制顶点数。
枚举类:当类的对象有限个时候。如果枚举类只有一个对象,可以作为单例模式的实现方式。
5.0之前(反编译):
public class Status{
private final String NAME;
private Status(String name){
this.NAME=name;
}
private static final Status FREE=new Status("FREE");
public String getName(){
return NAME;
}
}
5.0之后
public class Status{
SPRING("春天","春暖花开")
private final String seasonName;
private final String seasonDesc;
private Status(String seasonName,String seasonDesc){
this.seasonName=seasonName;
this.seasonDesc=seasonDesc;
}
get方法
常用方法:values()返回枚举类型的对象数组。valueOf(String)字符串转为对应枚举对象。toString()返回当前枚举对象常量的名称。
当枚举类实现接口时候想常量名不同同一方法的输出不同:
SPRING("春天","春暖花开"){
@Override
xxxx
}
六、注解
可以在框架中代替代码和XML配置等。
注解可以修饰包、类、构造器、方法、成员变量、参数、局部变量的声明。
如何自定义注解:
1.直接声明为@interface
2.内部定义成员,通常为value表示
3.可以指定成员的默认值,使用default定义
4.如果自定义注解没有成员,表明是一个标识作用
如果注解有成员,在使用注解时,需要指定成员值。
自定义注解必须配上注解的信息处理流程(反射)才有意义。
元注解:修饰其他注解的注解。
Retention:指定所修饰的Annotation的生命周期。SOURCE/CLASS(默认)/RUNTIME,只有RUNTIME的注解才能通过反射获取。
Target:指定注解能够修饰哪些元素
Documented:表示这个注解能在被javadoc解析时保存下来
Inherited:被它修饰的直接能够被继承。
JDK8新增可重复注解。要在定义注解时候加@Repeatable(定义的注解.class)。
另外JDK8也新增了类型注解,ElementType.TYPE_PARAMETER表示该注解能写在变量类型的声明语句中(如:泛型声明)。ElementType.TYPE_USE表示该注解能写在使用类型的任何语句中。
七、泛型
如果子类继承的父类指定了泛型,则实例化子类时,不再需要指明泛型。
1.泛型类可能有多个参数,此时应将多个参数一-起放在尖括号内。比如:<E1,E2, E3>
2.泛型类的构造器如下: public GenericClass(){}。
而下面是错误的: public GenericClass(){}
3.实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。
4.泛型不同的引用不能相互赋值。
尽管在编译时ArrayList和ArrayList是两种类型,但是,在运行时只有一个ArrayList被加载到JVM中。
5.泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。经验:泛型要使用一路都用。要不用,一路都不要用
6.如果泛型结构是-一个接口或抽象类,则不可创建泛型类的对象。
7. jdk1.7,泛型的简化操作: ArrayList fist = new ArrayList<>();
8.泛型的指定中不能使用基本数据类型,可以使用包装类替换。
9.在类/接口.上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态.属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型。
10.异常类不能是泛型的
11.不能使用rjew E[]。但是可以: E[] elements = (E[)new Object[capacity];
参考: ArrayList源码中声明: Object[] elementData,而非泛型参数类型数组。
12.父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:
子类不保留父类的泛型:按需实现
没有类型擦除 和 具体类型
子类保留父类的泛型:泛型子类
全部保留 部分保留

泛型方法和类是否是泛型类无关。
定义形参带泛型的方法:
public <E> List <E> copy (E[]arr){
这样声明,要不会认为是有个E的类
}
泛型在继承方面的体现:
类A是类B的父类 G<A>和G<B>没有关系。
A<G>是B<G>的父类。
注意:List<?> list=new ArrayList();这样用通配符的容器,只能add null,其他类型的对象不能add。允许读取数据,类型为Object。
有限制条件的通配符(?): <? extends 类> 可以理解为:?<= 类
<? super 类>可以理解为:?>= 类
八、IO流
File类使用:构造器三种。1.new File(相对路径/绝对路径) 2.new File(父路径,子路径) 3…new File( parent File,子文件路径)。(注意windos和Dos系统默认分隔符\,unix和url用/) (注意:是在内存中创建的)
String getAbsolutePath():获取绝对路径
String getPath():获取路径
String getName():获取名称
String getParent():获取上层文件目录
long length():获取文件长度(字符数)
long lastModified();最后一次修改时间
String[] list():获取指定目录下所有的文件或文件目录的名称数组
File[] listFiles():获取指定目录下的所有文件或文件目录的File数组
boolean isDirectory():是否为文件夹
boolean isFile():是否为文件
boolean exists():是否存在
boolean canRead():是否可读
boolean canWrite():是否可写
boolean isHidden():是否隐藏
boolean createNewFile():创建文件。若文件存在则不创建,返回
boolean mkdir():创建文件目录。如果存在就不创建。如果上层文件夹不存在也不创建。
boolean mkdirs():创建文件目录。如果存在就不创建。如果上层文件夹不存在一并创建。
boolean delete():删除文件或文件夹。注意java中的删除不走回收站。
流的分类:字节流 InputStream, OutputStream
字符流Reader、Writer
文件流:FileInputStream,FileOutputStream,FileReader,FileWriter
缓冲流:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter 提高读写效率。
InputStream/Reader使用:new FileReader(File).read()返回一个字符的int类型。如果到达末尾返回-1. read(char [ ] buffer):每次读取buffer数组中的字符个数,如果到达文件末尾返回-1.
OutputStream/Writer使用:new FileFileWriter(File,true).write(String) true表示追加模式,如果不用,默认为false覆盖模式。
File f1=new File("");
File f2=new File("");
BufferedInputStream is=new BufferedInputStream(new InputStream(f1));
BufferedOutputStream os=new BufferedOutputStream(new OutputStream(f2));
byte[] buffer=new byte[1024];//字符流换成char[]
int len=0;
while((len=is.read(buffer))!=-1){//字符流可以用data=is.readLine()!=null,其中data为String
os.write(buffer,0,len);
os.flush();//刷新缓冲区
}
os.close();
is.close();
转换流 InputStreamReader和OutputStreamWriter(构造时可以指定charset,不写则用系统默认的 )
字符集说明:
ASCII:一个字节的7位
ISO8859-1:一个字节
GB2312:最多两个字节
GBK:最多两个字节
Unicode:所有文字两个字节
UTF-8:变长编码方式。1-4字节
标准的输入输出流:System.in(InputSteam) 默认从键盘输入 System.out (OutputSteam)默认从控制台输出。重定向方法:setIn和setOut
数据流:DataInputStream和DataOutputStream分别套在InputStream和OutputStream。有一堆(read基本数据类型/String)和(write基本数据类型/String)。
对象流:ObjectInputStream(反序列化:读取), ObjectOutputStream(序列化:保存,Java对象变为二进制流):可以将java对象写入与读取。不能序列化static和transient修饰的成员变量。(要实现Serializiable接口或Externalizable(用的少))
实现Serializiable接口之后,还需要一个public static final long serialVersionUID=xxxxxxxL; 来标识这个类。这是用来表明类的不同版本之间的兼容性。简而言之目的是以序列化对象进行版本控制,有关各版本反序列化时是否兼容。如果没有显示定义,则由Java运行时环境根据类的细节自动生成的,但是会有可能出现问题。简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。
另外,除了这个类要实现Serializiable,其他属性也要可序列化。(基本数据类型也是可序列化的)
RandomAccessFile类(随机读取文件流):既可输入也可输出,可以跳到文件的任意位置读写文件。构造器需要指定访问模式:r只读,rw读写,rwd读写同步内容更新,rws读写同步内容与元数据更新。(作用:下载不用从头下载而是继续下载)
作为输出流存在:如果文件不存在创建,如果存在则从头开始覆盖,原有文件其他未被覆盖内容不变。(如果实现插入,需要先读取不想被覆盖的,之后再写)
自由移动指针:void seek(long pos)移动到指定位置,getFilePoint()获得当前位置。
九、网络编程
Java中用InetAddress类代表IP:
InetAddress inet1=InetAddress.getByName("IP/域名");//还存在一个getLocalhost
inet1.getHostName();//获得域名
inet1.getHostAddress();//获得IP
TCP通信实例:
//服务器端
ServerSocket ss=new ServerSocket(端口号);
Socket socket=ss.accept();//阻塞,但并不是因为io阻塞
InputStream is=socket.getInputStream();
FileOutputSteam out= new FileOutputSteam(File(""));
byte[]buffer =new byte[1024];
int len;
while((len=is.read(buffer))){//阻塞
out.write(buffer,0,len);
}
OutputStream os=socket.getOutputSream();
os.write("服务器已收到".getBytes());
os.close();
out.close();
is.close();
socket.close();
ss.close();
//客户端
Socket socker=new Socket(InetAddress.getByName("IP/域名"),端口号);
OutputSteam os=socker.getOutputStream();
FileOutputSteam fis=new FileOutputSteam(new File("xxx"));
byte[]buffer =new byte[1024];
int len;
while((len=fis.read(buffer))){
os.write(buffer,0,len);
}
socket.shutdownOutput();//这里需要关闭的原因在于FileOutputSteam是一个阻塞流,会不知道什么时候结束。
InputSteam is=socket.getInputStream();
ByteArrayOutputStream baos=new ByteArrayOutputStream();
byte[]buffer1 =new byte[10];
int len1;
while((len1=is.read(buffer))){
out.write(buffer,0,len);
}
System.out.println(baos.toString);
out.close();
baos.close();
is..close();
fis.close();
os.close();
socket.close();
其中使用的就是BIO,阻塞IO,只有当得到了连接(socket.accept()),得到了数据(is.read())才会继续往下进行。而且第一个客户端来了,一直没有结束,其他客户端就无法进行交互,只有在第一个客户端关闭了之后才会接收第二个客户端的请求。如果想用BIO来进行多客户端,那么就将阻塞的点放到多线程中,如将(is.read())放到多线程中。基于线程驱动模型
而NIO就只需要一个线程就能够解决。NIO中使用ServerSocketChannel、SocketChannel、selector(多路复用器/事件轮询器,来监听到底那个客户端有读写操作)。基于事件驱动模型(Reactor是其一种实现方式),需要事件、事件处理器、事件轮询。(BIO中使用的是ServerSocket和Socket)
NIO的核心包括Channel,Selector,Buffer。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。NIO将数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
NIO 是利用了单线程轮询事件的机制,通过高效地定位就绪的 Channel,来决定做什么,仅仅 select 阶段是阻塞的,可以有效避免大量客户端连接时,频繁线程切换带来的问题,应用的扩展能力有了非常大的提高。
优化NIO可以用多个线程NIO中的各个事件分开,一个线程做一个事件。
(Java AIO(NIO.2))异步非阻塞IO:
在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。
BIO、NIO、AIO适用场景分析:
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
UDP通信实例
发送端
DatagramSocket socket=new DatagramSocket();
String str="消息";
byte[]data=str.getBytes();
DatagramPacket packet=new DatagramPacket(data,0,data.length,端口号);
socket.send(packet);
socket.close();
接收端
DatagramSocket socket=new DatagramSocket(端口号);
byte[] buffer=new int[100];
DatagramPacket packet=new DatagramPacket(data,0,data.length);
socket.receive(packet);
socket.close();
URL组成:<传输协议>:<主机号>:<端口号>:<文件名>#片段名?参数列表
URL url=new URL("");
getProtocol();获得协议 getHost();获得主机名 getPort();获得端口号getPath();获得文件路径 getFile();获得该URL文件名 getQuery():获取该URL查询名
HttpURPConnection urlC=(HttpURPConnection) url.openConnection;
urlC.connection();
urlC.getInputStream();获得流之后流操作
十、反射
Reflection是Java从静态语言到准动态语言(动态语言允许在运行时改变结构)的关键。反射机制允许程序在执行期借助于Reflection API
取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。(反射可以直接访问private类而不是get,set方法) Class c=Class.forName(“java.lang.String”)
加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构,这个对象就像一面镜子,通过这个镜子
看到类的结构,所以称之为:反射。
正常方式:import类的包->new 实例化->取得实例化对象
反射方法:实例化对象->getClass()方法->得到完整的“包类”名称
Java反射优点和缺点
优点:可以实现动态构建对象和编译,体现出很大的灵活性
缺点:对性能有影响。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且它满足我们的要求。这类操作总是慢于直接执行相同的操作。
1.类加载过程:javac.exe命令后,会生成一个或多个字节码文件(.class),接着我们使用java.exe命令对某个字节码文件进行解释运行。相当于将某个字节码文件加载到内存中。此过程就称为类的加载。加载到内存中的类,我们就称为运行时类,就作为Class的一个实例。
加载:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象。
链接:将Java类的二进制代码合并到JVM的运行状态之中的过程。验证:确保加载的类信息符合JVM规范,没有安全方面的问题。准备:正式为静态变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程
初始化:执行构造器()方法的过程。类构造器()方法是由编译器自动收集类中所有类变量的赋值动作和静态码块中的语句合并产生的.(类构造器是构造类信息的,不是构造该类对象的构造器)。 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。
Class类的创建方式有哪些
//方式1:通过对象获得
Class c1=person.getClass();
//方式2:forname获得 最常用!!!
Class c2 = Class.forName("reflection.Person");
//方式3:通过类名.class获得
Class<Student> c3 = Student.class;
//方式4:使用类加载器
ClassLoader classLoader =当前类.class.getClassLoader();
Class c4=classLoader.loadClass(想获得的Class的包名);
//基本内置类型的包装类都有一个Tpye属性
Class<Integer> c5 = Integer.TYPE;
//获得父类类型
Class c6 = c1.getSuperclass();
哪些类型可以有class
Class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类 interface:接口 [ ]:数组 emum:枚举 annotation:注解 primitive type:数据类型 void
只要数组的元素类型和维度相同就是同一个Class
new Instance()根据Class创建对象(调用空参构造)。且要求空参构造访问权限够。通常设置为public。(JDK1.9以后一般用
Class.getDeclaredConstructor().new Instance())
JavaBean中要求空参构造原因:1.便于反射创建对象。2.便于子类继承时候,默认调用super时父类有此构造。
Field[] fields = c1.getFields();//获得本类及其父类的public属性
Field[] fields = c1.getDeclaredFields();//获得本类的所有属性
for(Field f:fields){
f.getModifies();//获取修饰符
f.getType();//获取数据类型
f.getName();//获取名字
}
Method[] methods=c1.getMethods();//获得本类及其父类的所有public方法
Method[] methods =c1.getDeclaredMethods();//获取本类的所有方法,包括私有的
for(Method m:methods){
Annotation[] ann=m.getAnnotations();//获得方法注解
m.getModifies();//获取修饰符
m.getReturnType();//获取返回值类型
m.getName();//获取方法名
Class [] ParameterTypes= m.getParameterTypes();//获得参数列表(如想使用需要判断ParameterTypes的==null?和ParameterTypes.length==0?)
}
Class[] ExceptionTypes= []m.getExceptionTypes();//使用方法要判断ExceptionTypes.length>0
Constructor[] constructor = c1.getConstructors();//获取当前类public构造器
Constructor[] constructor = c1.getDeclaredConstructors();//获取当前类所有构造器
Class father=c1.getSuperclass();//获得运行时父类
Type father=c1.getGenericSuperclass();//获得带泛型的父类
ParameterizedType paramType=(ParameterizedType) father;//获取结构化参数类型
Type[] actualTypeArguments=paramType.getActuralTypeArguments();//获取真实类型泛型类型,想用名字可以遍历getTypeName
Class[] interfaces=clazz.getInterfaces();//获得接口的Class
Package pack=clazz.getPackage();//获取所在包
Annotation[] annotations = c1.getAnnotations();//获得运行时类声明的注解
Field id=c1.getField("name");//获得指定public属性
id.set(值);
id.get(值);
Field id=c1.getDeclaredField("name");//获得指定属性可以获得private
id.setAccessible(true);//允许访问
Method m = c1.getMethod("getName");//获取指定public方法
Method m = c1.getDeclaredMethod("getName");//获取指定方法
m.setAccessible(true);//private方法要允许访问
返回值=m.invoke(方法调用者,参数args);//返回值=方法调用者.m(参数args)
返回值=m.invoke(定义static方法类.class,参数args);//使用静态方法1
返回值=m.invoke(null,参数args);//使用静态方法2
Constructor[] constructor = c1.getConstructors();//获得构造器
Constructor[] constructor = c1.getDeclaredConstructors();//获得构造器
c1.getConstructor(String.class, int.class, int.class);//获得指定构造器
c1.getDeclaredConstructor(String.class, int.class, int.class);//获得指定构造器
constructor.setAccessible(true);//允许访问
constructor.newInstance(构造器参数);
十一、Java8新特性
1.Lambda表达式
本质是接口的实例,要求接口为函数式接口(只有一个方法的接口,也可以显示标记@FunctionalInterface(这个注解只是检查是否是函数式接口,同时生成文档时说明是函数式接口))
->:Lambda操作符,左边是形参列表(接口中的抽象方法的形参列表),右边是重写的抽象方法的方法体。
//无参,无返回
Runnable r=()->{方法体};//当只有一行时,大括号可以省
//有一个参数无返回
Consumer r=(参数)->{方法体};//当只有一行时,大括号可以省
//只有一个参数,数据类型可省,编译器自动推断
Consumer<泛型> r=(一个参数)->{方法体};//当只有一行时,大括号可以省
//只有一个参数小括号可以省
Consumer r=参数->{方法体};//当只有一行时,大括号可以省
//多个参数有返回
Consumer r=(参数, , ,)->{方法体;最后return xxx};
//当Lambda体只有一条语句时,return和大括号都可省
Consumer r=(参数, , ,)->要返回的值或表达式;
当要传递给Lambda体的操作已经有实现的方法了,可以使用方法的引用。方法引用的本质就是Lambda表达式。使用格式: 类(或对象)::方法名
使用情节:已经有的类的方法的形参和返回值和函数式接口的方法的形参和返回值类型一样。
构造器引用:函数式接口的抽象方法的形参列表和构造器的形参列表一致。抽象方法的返回值类型即为构造器所属类的类型。
Supplier<Employee> sup1=()->new Employee();
Supplier<Employee> sup2=Employee::new;
Function <Integer,String[]> fun=length->new String[length];
String[] arr=fun.apply(5);
//数组引用
Function <Integer,String[]> fun=String :: new;
String[] arr=fun.apply(5);
2.Stream
使用StreamAPI对集合数据进行操作就类似于使用SQL执行的数据库查询。Stream和Collection的区别:Collection是一种静态的内存数据结构和内存有关,而Stream是有关计算的和CPU有关。
Steam自己不会存储元素。不会改变源对象,相反会返回一个持有结果的新Steam。Stream操作是延迟执行的,这意味着他们会等到需要结果的时候才执行。
Stream的操作三个步骤:1.创建Stream(一个数据源如集合数组,获取一个流)2.中间操作(一个中间操作链,对数据源的数据进行处理)3.停止操作(终端操作)一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用。
创建:
//方式1 通过集合
List<泛型> list=new xxxxx;
Stream<list的泛型> stream1=list.stream();//这种方法返回的是一个顺序流
Stream<泛型> stream2=list.parallelStream();//返回的是一个并行流不保证顺序
//方式2 通过数组
Arrays.stream(数组);// 返回值Stream<>或基本数据类型Stream
//方式3通过Sream的of()
IntStream stream=Stream.of(1,2,3,4);
//方式4 创建无限流
//迭代 public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
Stream.iterate(0,t->t+2).limit(10);//遍历前10个偶数
//生成 public static<T> Stream<T> generate(Supplier<T> s)
Stream.generate(Math::random).limit(10);
中间操作:
1.筛选与切片
stream.filter(e->e.getSalary()>7000);//过滤 小于7000的过滤掉
stream.limit(3);//截断流 使其元素不超过给定数量
stream.skip(10);//跳过前10个元素 如果流个数小于跳过的则为空
stream.distinct();//筛选通过hashCode和equals方法去重
2.映射
stream.map(str->str.toUppCase())//map(Function f)接收一个函数作为参数,将元素转换成其他形式或提取信息,该函数会被应用到每个元素上,并将映射成一个新元素
//flatmap(Function f)接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流 和上面map的区别 map->list.add() flatmap->list.addAll()
3.排序
stream.sorted();自然排序
stream.sorted(Comparator com);定制排序
终止操作:终端操作会从流的流水线生成结果,其值可以为任何不是流的值,例如List,Integer甚至是void。当用过终止操作后stream就消失了,需要重新得到stream
boolean stream.allMatch(lambda);检查是否匹配所有元素 boolean stream.anyMatch(lambda);检查是否至少匹配一个元素 boolean stream.noneMatch(lambda);检查是否没有匹配元素 findFirst()返回第一个元素 findAny()返回任意一个元素 count()返回元素个数 max(Comparator c)返回流中最大值 min(Comparator c)返回流中最小值 forEach(lambda)内部迭代
终止操作–归约reduce(T identity, BinaryOperator)可以将流中元素反复结合起来得到一个值返回类型为T,identity是初始索引。或reduce(BinaryOperator)
终止操作–收集collect(Collector c) 返回集合,集合类型由Collector决定
stream.collector(Collectors.toList());
Optional类是一个容器,能够检测对象是否为null,可以避免空指针异常。
本文深入讲解Java编程语言的核心概念和技术,包括多线程、泛型、反射、异常处理、网络编程、I/O流、字符串操作、注解、枚举、Lambda表达式、Stream API等,旨在帮助读者全面掌握Java开发的关键技能。
5643

被折叠的 条评论
为什么被折叠?



