不得不说,Lab3真的致命
学了很多东西,还没有详细整理,先放到这里咯。
java StringBuffer去除最后一位字符
根据分割符拼接字段,想去除最后一位分割符,可使用StringBuffer自带的deleteCharAt方法。
StringBuffer sb = new StringBuffer();
sb.append("a").append('\001').append("b").append('\001').append("c").append('\001').append("d").append('\001');
sb.deleteCharAt(sb.length()-1);
System.out.println(sb.toString());
在书写的过程中发现了泛型无法适用于重载的场景,报了both methods have same erasure的错误:
场景1. 当两个重载函数的参数如下
void func(Map<Integer, String> map) {}
void func(Map<Integer, List<String>> map) {}
//IDE会报出编译错误:both methods have same erasure
查询后发现网上写的原因是:
Map<String, Integer>的泛型参数无法提取出来,因为对于任意的Map<Key, Value>,在运行时都会产生类型擦除,可以在运行时通过getclass()检查Map中的对象类型。但并不意味着可以在运行时提取Key和Value的类型。
在
void func(Map<Integer, String> map) {}
void func(Map<Integer, List<String>> map) {}
中,由于Java泛型在编译时进行erase,上述方法都会变成void fanc(Map map),
场景2:
class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
class Apple {
private String name;
private int age;
public Apple(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
@Test
void test() {
List<Dog> dogList = Lists.newArrayList();
dogList.add(new Dog("Dog B", 2));
dogList.add(new Dog("Dog A", 2));
dogList.add(new Dog("Dog A", 1));
specialSort(dogList);
System.out.println(JSON.toJSONString(dogList));
List<Apple> appleList = Lists.newArrayList();
appleList.add(new Apple("Apple B", 2));
appleList.add(new Apple("Apple A", 2));
appleList.add(new Apple("Apple A", 1));
specialSort(appleList);
System.out.println(JSON.toJSONString(appleList));
}
public static void specialSort(List<Dog> dogList) {
dogList.sort((o1, o2) -> {
int result = 0;
if (o1.getAge() != o2.getAge()) {
result = o1.getAge() - o2.getAge();
} else {
Objects.requireNonNull(o1.getName());
Objects.requireNonNull(o2.getName());
result = ObjectUtils.compare(o1.getName(), o2.getName());
}
return result;
});
}
public static void specialSort(List<Apple> appleList) {
appleList.sort((o1, o2) -> {
int result = 0;
if (o1.getAge() != o2.getAge()) {
result = o1.getAge() - o2.getAge();
} else {
Objects.requireNonNull(o1.getName());
Objects.requireNonNull(o2.getName());
result = ObjectUtils.compare(o1.getName(), o2.getName());
}
return result;
});
}
报错:
‘specialSort(List)’ clashes with ‘specialSort(List)’;both methods have same erasure
下面进行具体学习↘
Java泛型: 类型擦除(type erasure)
Java的泛型不同于C++的模板:Java泛型是"type erasure",C++模板是"reified generic"。
· type erasure:泛型类型仅存在于编译期间,编译后的字节码和运行时不包含泛型信息,所有的泛型类型映射到同一份字节码。 (object)
· reified generic:泛型类型存在于编译和运行期间,编译器自动为每一种泛型类型生成类型代码并编译进二进制码中。
type erasure的本质
泛型(T) --> 编译器(type erasure) --> 原始类型(T被Object替换)
泛型(? extends XXX) --> 编译器(type erasure) --> 原始类型(T被XXX替换)
原始类型指被编译器擦除了泛型信息后,类型变量在字节码中的具体类型。
假如,我们定义一个泛型类Generic
是这样的:
class Generic<T> {
private T obj;
public Generic(T o) {
obj = o;
}
public T getObj() {
return obj;
}
}
那么,Java编译后的字节码中genatic相当于这样的
class Generic {
private Object obj;
public Generic(Object o) {
obj = o;
}
public Object getObj() {
return obj;
}
}
假如,我们使用Generic
类是这样的:
public static void main(String[] args) {
Generic<String> generic = new Generic<String>("hehe...");
String str = generic.getObj();
}
那么,Java编译后的字节码中相当于这样的:
public static void main(String[] args) {
Generic generic = new Generic("hehe...");
String str = (String) generic.getObj();
所以,所有Generic
的泛型类型实质是同一个类:
public static void main(String[] args) {
Generic<Integer> a = new Generic<Integer>(111);
Generic<String> b = new Generic<String>("bbb");
System.out.println("a'class: " + a.getClass().getName());
System.out.println("b'class: " + b.getClass().getName());
System.out.println("G'class: " + Generic.class.getName());
System.out.println("a'class == b'class == G'class: " + (a.getClass() == b.getClass() && b.getClass() == Generic.class));
}
上述代码执行结果:
a'class: generic.Generic
b'class: generic.Generic
G'class: generic.Generic
a'class == b'class == G'class: true
小结:Java的泛型只存在于编译时期,泛型使编译器可以在编译期间对类型进行检查以提高类型安全,减少运行时由于对象类型不匹配引发的异常。
type erasure导致泛型的局限性
类型擦除降低了泛型的泛化性,使得某些重要的上下文环境中不能使用泛型类型,具有一定的局限性。
运行时隐含类型转换的开销
使用泛型时,Java编译器自动帮我们生成了类型转换的代码,这相对于C++模板来说无疑带来了额外的性能开销。
类型参数不能实例化
T obj = new T(); // compile error
T[] objs = new T[10]; // compile error
Generic<String> generic = new Generic<String>[10]; // compile error
//实例化概念有些模糊
类型参数不能进行类型查询(类型查询在运行时,运行时类型参数已被擦除)
Generic<Integer> a = new Generic<Integer>(111);
if(a instanceof Generic<String>)// compile error
if(a instanceof Generic<T>) // compile error
if(a instanceof Generic) // 仅测试了a是否是Generic,忽略了类型参数
不能在静态域和静态方法中引用类型变量
class Generic<T> {
private static T obj;// compile error
public static T func(){...}// compile error
}
因为所有泛型类最终映射到同一个原始类型类,而静态属性是类级别的(?),类和实例共同拥有它的一份存储,因此一份存储无法安放多个类型的属性。静态方法也是如此。
重载方法签名冲突:
public boolean equals(T obj) // compile error
public boolean equals(T obj)被擦除类型后变为public boolean equals(Object obj),与根类Object的public boolean equals(Object obj)签名一样,而两者均不能覆盖对方,导致编译期名称冲突
一个类不能实现同一个泛型接口的两种变体:
interface IFace<T>() {}
class FaceImpParent implements IFace<String> {}
class FaceImpChild extends FaceImpParent implements IFace<Integer> {} // compile error
原因是IFace<String>
和IFace<Integer>
在擦除类型后是同一个接口,一个类不能实现两次同一个接口。
泛型类不能扩展java.lang.Throwable
class GenericException <T> extends Exception {} // compile error
对于场景2的解决方案:
解决方案
方案一:针对集合对象,方法声明不同基类/实现类作为方法参数类型(不推荐)
既然List
和List
在编译时都回变成List
,如果我已知其中一个集合对象是ArrayList
对象,以List
为例,那就可以将其对应的方法参数类型改成ArrayList
。再或者,直接将其中一个方法改成Collection
。
如此,因为参数类型不同,便满足重载的条件。
个人认为,这样的做法会大大影响代码的可读性,且会造成大量的冗余代码,虽然能解决问题,但真的不推荐。
此方案参考自:both methods have same erasure:如何无损扩展代码
// Class Dog 和 Class Apple 的定义不变
@Test
void test() {
ArrayList<Dog> dogList = Lists.newArrayList();
dogList.add(new Dog("Dog B", 2));
dogList.add(new Dog("Dog A", 2));
dogList.add(new Dog("Dog A", 1));
specialSort(dogList);
System.out.println(JSON.toJSONString(dogList));
ArrayList<Apple> appleList = Lists.newArrayList();
appleList.add(new Apple("Apple B", 2));
appleList.add(new Apple("Apple A", 2));
appleList.add(new Apple("Apple A", 1));
specialSort(appleList);
System.out.println(JSON.toJSONString(appleList));
}
public static void specialSort(List<Dog> dogList) {
dogList.sort((o1, o2) -> {
int result = 0;
if (o1.getAge() != o2.getAge()) {
result = o1.getAge() - o2.getAge();
} else {
Objects.requireNonNull(o1.getName());
Objects.requireNonNull(o2.getName());
result = ObjectUtils.compare(o1.getName(), o2.getName());
}
return result;
});
}
public static void specialSort(ArrayList<Apple> appleList) {
appleList.sort((o1, o2) -> {
int result = 0;
if (o1.getAge() != o2.getAge()) {
result = o1.getAge() - o2.getAge();
} else {
Objects.requireNonNull(o1.getName());
Objects.requireNonNull(o2.getName());
result = ObjectUtils.compare(o1.getName(), o2.getName());
}
return result;
});
}
方案二:设置共同基类/接口,抽象出方法所需要内容
如果两个是类型相近的类,如Dog
和Cat
,那么可以考虑新建一个公共基类,随后在基类中暴露出为方法所需要的属性获取方法。
在可重现问题代码
中,方法目的是为了按特殊规则排序,那么也可以选择直接在基类中实现Comparable
接口,重写比较的方法。
但对于类型不相近,或较难抽象出公共基类的类,那这个方案就显得不适用了。
abstract class ParentClass {
public abstract String getName();
public abstract int getAge();
}
class Dog extends ParentClass{
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
class Apple extends ParentClass{
private String name;
private int age;
public Apple(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
@Test
void test() {
List<Dog> dogList = Lists.newArrayList();
dogList.add(new Dog("Dog B", 2));
dogList.add(new Dog("Dog A", 2));
dogList.add(new Dog("Dog A", 1));
specialSort(dogList);
System.out.println(JSON.toJSONString(dogList));
List<Apple> appleList = Lists.newArrayList();
appleList.add(new Apple("Apple B", 2));
appleList.add(new Apple("Apple A", 2));
appleList.add(new Apple("Apple A", 1));
specialSort(appleList);
System.out.println(JSON.toJSONString(appleList));
}
public static <T extends ParentClass> void specialSort(List<T> list) {
list.sort((o1, o2) -> {
int result = 0;
if (o1.getAge() != o2.getAge()) {
result = o1.getAge() - o2.getAge();
} else {
Objects.requireNonNull(o1.getName());
Objects.requireNonNull(o2.getName());
result = ObjectUtils.compare(o1.getName(), o2.getName());
}
return result;
});
}
方案三:在方法中,直接使用映射机制//需进一步学习映射机制
在我看来,这是最暴力的方案了,方法中先可以直接通过映射机制获取到对象的属性值,随后便可以进行原来的逻辑代码操作了。
这个方案,不需要重载,省去了那些冗余的逻辑代码,只是反射机制需要格外的开销。
// Class Dog 和 Class Apple 的定义不变
@Test
void test() {
List<Dog> dogList = Lists.newArrayList();
dogList.add(new Dog("Dog B", 2));
dogList.add(new Dog("Dog A", 2));
dogList.add(new Dog("Dog A", 1));
specialSort(dogList);
System.out.println(JSON.toJSONString(dogList));
List<Apple> appleList = Lists.newArrayList();
appleList.add(new Apple("Apple B", 2));
appleList.add(new Apple("Apple A", 2));
appleList.add(new Apple("Apple A", 1));
specialSort(appleList);
System.out.println(JSON.toJSONString(appleList));
}
public static void specialSort(List list){
list.sort((o1, o2) -> {
int result = 0;
// 暂不考虑特殊异常处理
try {
String method = "getAge";
int age1 = (int) o1.getClass().getMethod(method).invoke(o1),
age2 = (int) o2.getClass().getMethod(method).invoke(o2);
if (age1 != age2) {
result = age1 - age2;
} else {
method = "getName";
String name1 = (String) o1.getClass().getMethod(method).invoke(o1),
name2 = (String) o2.getClass().getMethod(method).invoke(o2);
Objects.requireNonNull(name1);
Objects.requireNonNull(name2);
result = ObjectUtils.compare(name1, name2);
}
} catch (Exception e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
return result;
});
}
但上述描述的都是在同一个class中重载函数导致的编译错误,我的是在接口和实现类中发生冲突,经检查是实现类在implement接口时少了一个<L>......whatever,学到了新东西了就是。
查询了treeset及其相对应的排序方法,且其排序方法的重写可利用于list(同属Collection集合)
具体实现见:https://www.jianshu.com/p/12f4dbdbc652
list的排序方法见:https://www.cnblogs.com/Lxiaojiang/p/6805151.html
list重写与treeset相似
Set的四种排序方法:https://blog.csdn.net/the_fool_/article/details/82389351
学习了this的关键字详解:
- this( ) 不能在普通方法中使用,只能写在构造方法中。
- 在构造方法中使用时,必须是第一条语句。