• 转载
  • 前端开发
  • 每个Java开发人员都应该停止做的三件事

    2020.04.03 15:24发布

    6074人阅读

    0人评论



    从返回空值到过度使用getter和setter,甚至Java程序员都习惯于使用成语,即使在不需要的时候也是如此。尽管它们在某些情况下可能是适当的,但它们通常是使系统正常运行的习惯或后备力量。在本文中,我们将遍历Java开发人员(无论是新手还是高级)中的三件事,并探讨它们如何使我们陷入困境。应该注意的是 ,无论如何,这些并不是始终应遵守的严格规则。有时,使用这些模式来解决问题可能有充分的理由,但是总的来说,它们的使用量应该比现在少得多。首先,我们将以Java中最多产但双刃的关键字开始:Null。


    1.返回零


    空值一直是开发人员的最好的朋友,也是最大的敌人,Java中的空值也不例外。在高性能应用程序中,空值可以是减少对象数量并发出方法无返回值的可靠方式。与抛出异常(在创建异常时必须捕获整个堆栈跟踪)相反,null是一种快速,低开销的方式,向客户端发出无法获取任何值的信号。


    在高性能系统之外,通过为空返回值创建更繁琐的检查并NullPointerException在取消引用空对象时引起s ,空可能会对应用程序造成破坏 。在大多数应用程序中,返回空值的原因主要有以下三个:(1)表示找不到列表元素;(2)表示即使没有发生错误,也找不到有效值;或(3) )表示特殊情况下的返回值。


    除非有任何性能原因,上述每种情况都有一个更好的解决方案,该解决方案不使用null并强制开发人员处理null情况。此外,这些方法的客户也不会ing之以鼻,想知道该方法在某些情况下是否会返回null。在每种情况下,我们将设计一种更干净的方法,该方法不涉及返回空值。


    没有元素


    返回列表或其他集合时,通常会看到返回一个空集合,以表示找不到该集合的元素。例如,我们可以创建一个服务来管理数据库中的用户,该服务类似于以下内容(为简便起见,省略了一些方法和类定义):


    public class UserService {


    public List<User> getUsers() {


    User[] usersFromDb = getUsersFromDatabase();


    if (usersFromDb == null) {


    // No users found in database


    return null;


    }


    else {


    return Arrays.asList(usersFromDb);


    }


    }


    }


    UserServer service = new UserService();


    List<Users> users = service.getUsers();


    if (users != null) {


    for (User user: users) {


    System.out.println("User found: " + user.getName());


    }


    }


    由于我们选择了在没有用户的情况下返回空值,因此我们迫使客户端在遍历用户列表之前先处理这种情况。如果相反,我们返回了一个空列表以表示未找到用户,则客户端可以完全删除null检查并照常遍历用户。如果没有用户,则循环将被隐式跳过,而无需手动处理。从本质上讲,遍历用户列表的功能与我们想要的空列表和填充列表的功能相同,而无需手动处理一种情况或另一种情况:


    public class UserService {


    public List<User> getUsers() {


    User[] usersFromDb = getUsersFromDatabase();


    if (usersFromDb == null) {


    // No users found in database


    return Collections.emptyList();


    }


    else {


    return Arrays.asList(usersFromDb);


    }


    }


    }


    UserServer service = new UserService();


    List<Users> users = service.getUsers();


    for (User user: users) {


    System.out.println("User found: " + user.getName());


    }


    在上述情况下,我们选择返回一个不变的空列表。只要我们证明该列表是不可变的,并且不应该对其进行修改(这样做可能会引发异常),则这是可接受的解决方案。如果列表必须可变,我们可以返回一个空的可变列表,如以下示例所示:


    public List<User> getUsers() {


    User[] usersFromDb = getUsersFromDatabase();


    if (usersFromDb == null) {


    // No users found in database


    return new ArrayList<>(); // A mutable list


    }


    else {


    return Arrays.asList(usersFromDb);


    }


    }


    通常,在发出找不到元素的信号时,应遵循以下规则:


    返回一个空集合(或列表,集合,队列等),表示找不到任何元素


    这样做不仅减少了客户端必须执行的特殊情况处理,而且还减少了我们界面中的不一致情况(即,有时返回一个列表对象,而不返回其他对象)。


    可选值


    很多时候,当我们希望通知客户不存在可选值但没有发生错误时,会返回空值。例如,从网址获取参数。在某些情况下,该参数可能存在,但在其他情况下,可能没有。缺少此参数并不一定表示错误,而是表示用户不希望提供该参数时所包含的功能(例如排序)。我们可以通过以下方式处理此问题:如果不存在任何参数,则返回null;如果提供了参数,则返回该参数的值(为简便起见,某些方法已删除):


    public class UserListUrl {


    private final String url;


    public UserListUrl(String url) {


    this.url = url;


    }


    public String getSortingValue() {


    if (urlContainsSortParameter(url)) {


    return extractSortParameter(url);


    }


    else {


    return null;


    }


    }


    }


    UserService userService = new UserService();


    UserListUrl url = new UserListUrl("http://localhost/api/v2/users");


    String sortingParam = url.getSortingValue();


    if (sortingParam != null) {


    UserSorter sorter = UserSorter.fromParameter(sortingParam);


    return userService.getUsers(sorter);


    }


    else {


    return userService.getUsers();


    }


    如果未提供任何参数,则返回null,并且客户端必须处理这种情况,但是该getSortingValue 方法的签名中 都没有声明排序值是可选的。为了使我们知道此方法是可选的,如果不存在任何参数,则可能返回null,我们将必须阅读与该方法相关的文档(如果提供了任何文档)。


    相反,我们可以使可选性明确地返回一个 Optional 对象。正如我们将看到的,当没有参数存在时,客户端仍然必须处理这种情况,但是现在该要求已明确。而且,与Optional 简单的null检查相比,该类提供了更多的机制来处理缺少的参数。例如,我们可以简单地使用以下提供的查询方法(状态测试方法)检查参数是否存在Optional:


    public class UserListUrl {


    private final String url;


    public UserListUrl(String url) {


    this.url = url;


    }


    public Optional<String> getSortingValue() {


    if (urlContainsSortParameter(url)) {


    return Optional.of(extractSortParameter(url));


    }


    else {


    return Optional.empty();


    }


    }


    }


    UserService userService = new UserService();


    UserListUrl url = new UserListUrl("http://localhost/api/v2/users");


    Optional<String> sortingParam = url.getSortingValue();


    if (sortingParam.isPresent()) {


    UserSorter sorter = UserSorter.fromParameter(sortingParam.get());


    return userService.getUsers(sorter);


    }


    else {


    return userService.getUsers();


    }


    这几乎与null检查的情况相同,但是我们已经明确说明了参数的可选性(即,客户端不能在不调用的情况下访问参数get(),NoSuchElementException 如果可选为空,则会抛出a )。如果我们不希望基于网址中的可选参数返回用户列表,而是以某种方式使用该参数,则可以使用该 ifPresentOrElse 方法:


    sortingParam.ifPresentOrElse(


    param -> System.out.println("Parameter is :" + param),


    () -> System.out.println("No parameter supplied.")


    );


    这大大降低了空检查所需的噪声。如果我们希望在不提供任何参数的情况下忽略该参数,则可以使用以下 ifPresent 方法:


    sortingParam.ifPresent(param -> System.out.println("Parameter is :" + param));


    在这两种情况下,使用 Optional 对象而不是返回null都会显式强制客户端处理可能不存在返回值的情况,并提供更多途径来处理此可选值。考虑到这一点,我们可以设计以下规则:


    如果返回值是可选的,请确保客户端处理此情况,方法是返回Optional ,如果找到一个则返回一个包含值的值,如果找不到则返回一个 空值


    特殊情况值


    最后一个常见用例是特殊情况,即无法获得正常值,并且客户应处理与其他情况不同的特殊情况。例如,假设我们有一个命令工厂,客户机定期从中请求命令来完成。如果没有命令准备就绪,则客户端应等待1秒钟,然后再次询问。我们可以通过返回客户端必须处理的空命令来完成此操作,如下面的示例所示(为简洁起见,未显示某些方法):


    public interface Command {


    public void execute();


    }


    public class ReadCommand implements Command {


    @Override


    public void execute() {


    System.out.println("Read");


    }


    }


    public class WriteCommand implements Command {


    @Override


    public void execute() {


    System.out.println("Write");


    }


    }


    public class CommandFactory {


    public Command getCommand() {


    if (shouldRead()) {


    return new ReadCommand();


    }


    else if (shouldWrite()) {


    return new WriteCommand();


    }


    else {


    return null;


    }


    }


    }


    CommandFactory factory = new CommandFactory();


    while (true) {


    Command command = factory.getCommand();


    if (command != null) {


    command.execute();


    }


    else {


    Thread.sleep(1000);


    }


    }


    由于 CommandFactory 可以返回空命令,因此客户端必须检查接收到的命令是否为空,如果睡眠,则睡眠1秒钟。这将创建一组条件逻辑,客户端必须自行处理。我们可以通过创建一个空对象 (有时称为特例对象)来减少这种开销。空对象将原本在空场景中执行的逻辑(即休眠1秒)封装到一个以空情况返回的对象中。对于我们的命令示例,这意味着创建一个SleepCommand 在执行时进入 休眠状态的:


    public class SleepCommand implements Command {


    @Override


    public void execute() {


    Thread.sleep(1000);


    }


    }


    public class CommandFactory {


    public Command getCommand() {


    if (shouldRead()) {


    return new ReadCommand();


    }


    else if (shouldWrite()) {


    return new WriteCommand();


    }


    else {


    return new SleepCommand();


    }


    }


    }


    CommandFactory factory = new CommandFactory();


    while (true) {


    Command command = factory.getCommand();


    command.execute();


    }


    与返回空集合的情况一样,创建空对象使客户端可以隐式处理特殊情况,就好像它们是正常情况一样。但是,这并不总是可能的;在某些情况下,客户必须做出处理特例的决定。这可以通过允许客户端提供默认值来处理,就像使用 Optional 类一样。对于Optional,客户端可以使用以下orElse 方法获取包含的值或默认值 :


    UserListUrl url = new UserListUrl("http://localhost/api/v2/users");


    Optional<String> sortingParam = url.getSortingValue();


    String sort = sortingParam.orElse("ASC");


    如果有提供的排序参数(即,如果 Optional 包含一个值),则将返回此值。如果不存在任何值, "ASC" 则默认情况下将返回。的 Optional 类还允许客户端创建需要时的默认值,在情况下,默认创建过程是昂贵的(即,默认将仅创建需要时):


    UserListUrl url = new UserListUrl("http://localhost/api/v2/users");


    Optional<String> sortingParam = url.getSortingValue();


    String sort = sortingParam.orElseGet(() -> {


    // Expensive computation


    });


    结合使用空对象和默认值,我们可以设计以下规则:


    在可能的情况下,使用空对象处理空个案,或允许客户端提供默认值


    2.默认为函数式编程


    自从Java开发工具包(JDK)8中引入了流和lambda以来,就一直在向功能编程迁移,这一点是正确的。在使用lambda和stream之前,执行简单的功能任务很麻烦,并且导致代码严重无法阅读。例如,以传统风格过滤集合会产生类似于以下内容的代码:


    public class Foo {


    private final int value;


    public Foo(int value) {


    this.value = value;


    }


    public int getValue() {


    return value;


    }


    }


    Iterator<Foo> iterator = foos.iterator();


    while(iterator.hasNext()) {


    if (iterator.next().getValue() > 10) {


    iterator.remove();


    }


    }


    尽管这段代码很紧凑,但是如果满足某些条件,它不会以明显的方式告诉我们要删除集合中的元素。相反,它告诉我们,当集合中有更多元素时,我们正在迭代一个集合,如果每个元素的值大于10,则将其删除(我们可以推测正在发生过滤,但是在代码的冗长性中它被遮盖了) 。我们可以使用函数式编程将此逻辑缩减为一个语句:


    foos.removeIf(foo -> foo.getValue() > 10);


    该语句不仅比其迭代替代方案简洁得多,而且还准确地告诉我们它正在尝试执行的操作。如果我们命名谓词并将其传递给removeIf 方法,我们甚至可以使其更具可读性 :


    Predicate<Foo> valueGreaterThan10 = foo -> foo.getValue() > 10;


    foos.removeIf(valueGreaterThan10);


    该摘要的最后一行看起来像是英语中的句子,可 将该语句的执行情况准确告知我们 。由于代码看起来如此紧凑和易读,因此很容易在 需要迭代的每种情况下尝试和使用函数式编程 ,但这是幼稚的哲学。并非每种情况都适合进行功能编程。例如,如果我们尝试在一组纸牌(每个西装和军衔的组合)中打印这套西装和军衔的叉积,我们可以创建以下内容(有关以下内容的详细列表,请参见Effective Java,第3版)这个例子):


    public static enum Suit {


    CLUB, DIAMOND, HEART, SPADE;


    }


    public static enum Rank {


    ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING;


    }


    Collection<Suit> suits = EnumSet.allOf(Suit.class);


    Collection<Rank> ranks = EnumSet.allOf(Rank.class);


    suits.stream()


    .forEach(suit -> {


    ranks.stream().forEach(rank -> System.out.println("Suit: " + suit + ", rank: " + rank));


    });


    尽管阅读起来并不复杂,但这并不是我们可以设计的最直接的实现。显然,我们正在尝试将流引入传统迭代更为有利的领域。如果使用传统迭代,则可以将西服和军衔的叉积简化为以下内容:


    for (Suit suit: suits) {


    for (Rank rank: ranks) {


    System.out.println("Suit: " + suit + ", rank: " + rank);


    }


    }


    这种样式虽然不那么浮华,但更加直接。我们可以很快地看到,我们正在尝试遍历每件西服,并对每件西服进行等级排序和配对。流表达式越大,函数式编程的乏味就越明显。以Joshua Bloch在Effective Java,第3版(第205页,项目45)中创建的以下代码片段为例,以查找用户提供的路径中字典中包含的指定长度的所有字谜:


    public class Anagrams {


    public static void main(String[] args) throws IOException {


    Path dictionary = Paths.get(args[0]);


    int minGroupSize = Integer.parseInt(args[1]);


    try (Stream<String> words = Files.lines(dictionary)) {


    words.collect(


    groupingBy(word -> word.chars().sorted()


    .collect(StringBuilder::new,


    (sb, c) -> sb.append((char) c),


    StringBuilder::append).toString()))


    .values().stream()


    .filter(group -> group.size() >= minGroupSize)


    .map(group -> group.size() + ": " + group)


    .forEach(System.out::println);


    }


    }


    }


    即使是经验最丰富的流支持者,也可能会对这种实现方式感到沮丧。代码的意图尚不清楚,需要花费大量的精力才能发现上述流操作试图实现的目的。这并不意味着流很复杂或太罗word,但是它们并不 总是 最佳选择。正如我们在上面看到的,将 removeIf 简化后的一组语句简化为一个易于理解的语句。因此,我们不应该尝试 用流甚至lambda 代替 传统迭代的每个实例。相反,在决定是进行函数编程还是使用传统途径时,我们应遵循以下规则:


    函数式编程和传统迭代都有其优点和缺点:使用任何一种都会导致最简单和最易读的代码


    尽管在每种可能的情况下都可能会尝试使用Java最华丽,最新的功能,但这并不总是最好的方法。有时,老式功能最有效。


    3.创建不加选择的获取器和设置器


    新手程序员要教的第一件事是将与类关联的数据封装在私有字段中,并通过公共方法公开它们。在实践中,这将导致创建获取器以访问类的私有数据,并创建设置器以修改类的私有数据:


    public class Foo {


    private int value;


    public void setValue(int value) {


    this.value = value;


    }


    public int getValue() {


    return value;


    }


    }


    尽管这是一种适合新程序员学习的好习惯,但它也不应该不精通于中级或高级编程。在实践中通常发生的情况是,每个私有字段都具有一对getter和setter,从而将类的内部暴露给外部实体。这可能会导致一些严重的问题,尤其是在私有字段可变的情况下。这不仅是setter的问题,甚至是仅存在getter的问题。以下面的类为例,该类使用getter公开其唯一字段:


    public class Bar {


    private Foo foo;


    public Bar(Foo foo) {


    this.foo = foo;


    }


    public Foo getFoo() {


    return foo;


    }


    }


    由于我们明智地限制删除了二传手方法,因此这种暴露似乎是无害的,但距离它还很遥远。假设另一个类访问类型的对象 Bar 并在Foo 不Bar 知道该对象的情况下 更改其基础值 :


    Foo foo = new Foo();


    Bar bar = new Bar(foo);


    // Another place in the code


    bar.getFoo().setValue(-1);


    在这种情况下,我们在Foo 不通知Bar 对象的情况下更改了对象 的基础值 。如果我们提供的Foo 对象的值破坏了对象的不变性,则 可能导致一些严重的问题 Bar 。例如,如果我们有一个不变式,说明的值 Foo 不能为负,那么上面的代码片段会在不通知该Bar 对象的情况下静默破坏该不变式 。当该 Bar 对象使用其Foo 对象的值时 ,事情可能会飞快地往南走,尤其是如果该 Bar 对象 假定 不变量因未暴露设置器而直接将其重新赋值时,它就保持不变。 Foo 它持有的对象。如果严重更改数据,这甚至可能导致系统故障,例如以下数组意外暴露的示例:


    public class ArrayReader {


    private String[] array;


    public String[] getArray() {


    return array;


    }


    public void setArray(String[] array) {


    this.array = array;


    }


    public void read() {


    for (String e: array) {


    System.out.println(e);


    }


    }


    }


    public class Reader {


    private ArrayReader arrayReader;


    public Reader(ArrayReader arrayReader) {


    this.arrayReader = arrayReader;


    }


    public ArrayReader getArrayReader() {


    return arrayReader;


    }


    public void read() {


    arrayReader.read();


    }


    }


    ArrayReader arrayReader = new ArrayReader();


    arrayReader.setArray(new String[] {"hello", "world"});


    Reader reader = new Reader(arrayReader);


    reader.getArrayReader().setArray(null);


    reader.read();


    执行此代码将导致, NullPointerException 因为与ArrayReader 对象关联的数组 在尝试迭代该数组时为null。令人不安的 NullPointerException 是,它可能会在对进行更改后很长时间发生, ArrayReader 甚至可能在完全不同的上下文中发生(例如,在代码的不同部分,甚至可能在不同的线程中),从而导致了跟踪任务下来的问题非常困难。


    精明的读者可能还会注意到,我们本可以创建私有ArrayReader字段,final 因为在通过构造函数设置它之后,我们没有公开重新分配它的方法。尽管看起来这将使该ArrayReader常数不变,从而确保ArrayReader我们返回的对象无法更改,但事实并非如此。相反,添加 final 到字段只能确保不重新分配字段本身(即,我们无法为该字段创建设置器)。它不会阻止对象本身的状态被更改。如果我们尝试添加 final 到getter方法中,那么这也是徒劳的,因为 final 方法上的修饰符仅意味着该方法不能被子类覆盖。


    我们甚至可以更进一步和防守复制的 ArrayReader 对象的构造函数Reader,确保已传递到对象的对象不能与已经供给之后被篡改 Reader 的对象。例如,不会发生以下情况:


    ArrayReader arrayReader = new ArrayReader();


    arrayReader.setArray(new String[] {"hello", "world"});


    Reader reader = new Reader(arrayReader);


    arrayReader.setArray(null); // Change arrayReader after supplying it to Reader


    reader.read(); // NullPointerException thrown


    即使进行了这三个更改( final 字段上的final 修饰符,getter上的 修饰符以及ArrayReader 提供给构造函数的防御性副本 ),我们仍然没有解决问题。 我们如何公开类的基础数据并没有发现问题 ,而实际上我们是在这样做的。为了解决这个问题,我们必须停止公开类的内部数据,而是提供一种方法来更改基础数据,同时仍要遵守类不变式。以下代码解决了这个问题,同时引入了提供的防御性副本 ArrayReader 并标记了 ArrayReader 字段的final,这是正确的,因为没有设置器:


    public class ArrayReader {


    public static ArrayReader copy(ArrayReader other) {


    ArrayReader copy = new ArrayReader();


    String[] originalArray = other.getArray();


    copy.setArray(Arrays.copyOf(originalArray, originalArray.length));


    return copy;


    }


    // ... Existing class ...


    }


    public class Reader {


    private final ArrayReader arrayReader;


    public Reader(ArrayReader arrayReader) {


    this.arrayReader = ArrayReader.copy(arrayReader);


    }


    public ArrayReader setArrayReaderArray(String[] array) {


    arrayReader.setArray(Objects.requireNonNull(array));


    }


    public void read() {


    arrayReader.read();


    }


    }


    ArrayReader arrayReader = new ArrayReader();


    arrayReader.setArray(new String[] {"hello", "world"});


    Reader reader = new Reader(arrayReader);


    reader.read();


    Reader flawedReader = new Reader(arrayReader);


    flawedReader.setArrayReaderArray(null); // NullPointerException thrown


    如果我们查看有缺陷的阅读器, NullPointerException 仍然会抛出a,但是当不变量(读取时使用非空数组)被破坏时,它将立即抛出,而不是在以后的某个时间。这样可以确保不变式快速发生故障,从而使调试和查找问题根源变得更加容易。


    我们可以将这一原理更进一步,并指出,如果没有迫切需要允许更改类状态的方法,最好使类的字段完全不可访问。例如,我们可以Reader 通过删除创建后修改其状态的任何方法来使 类完全封装:


    public class Reader {


    private final ArrayReader arrayReader;


    public Reader(ArrayReader arrayReader) {


    this.arrayReader = ArrayReader.copy(arrayReader);


    }


    public void read() {


    arrayReader.read();


    }


    }


    ArrayReader arrayReader = new ArrayReader();


    arrayReader.setArray(new String[] {"hello", "world"});


    Reader reader = new Reader(arrayReader);


    // No changes can be made to the Reader after instantiation


    reader.read();


    将此概念归纳为逻辑结论,如果可能的话,使类不可变是一个好主意。因此,对象的状态在实例化之后永远不会改变。例如,我们可以创建一个不可变 Car 对象,如下所示:


    public class Car {


    private final String make;


    private final String model;


    public Car(String make, String model) {


    this.make = make;


    this.model = model;


    }


    public String getMake() {


    return make;


    }


    public String getModel() {


    return model;


    }


    }


    重要的是要注意,如果类的字段不是原始的,则客户端可以像上面看到的那样修改基础对象。因此,不可变对象应返回这些对象的防御性副本,从而使客户端无法修改不可变对象的内部状态。但是请注意,由于每次调用getter都会创建一个新对象,因此防御性复制会降低性能。不应过早优化此问题(忽略不变性以保证可能的性能提高),但应注意。以下代码段提供了方法返回值的防御性复制示例:


    public class Transmission {


    private String type;


    public static Transmission copy(Transmission other) {


    Transmission copy = new Transmission();


    copy.setType(other.getType);


    return copy;


    }


    public String setType(String type) {


    this.type = type;


    }


    public String getType() {


    return type;


    }


    }


    public class Car {


    private final String make;


    private final String model;


    private final Transmission transmission;


    public Car(String make, String model, Transmission transmission) {


    this.make = make;


    this.model = model;


    this.transmission = Transmission.copy(transmission);


    }


    public String getMake() {


    return make;


    }


    public String getModel() {


    return model;


    }


    public Transmission getTransmission() {


    return Transmission.copy(transmission);


    }


    }


    这使我们具有以下原则:


    使类不可变,除非迫切需要更改类的状态。不变类的所有字段都应标记为私有和最终字段,以确保不对字段执行任何重新分配,并且不应该提供对字段内部状态的间接访问


    不变性还带来了一些非常重要的优势,例如该类易于在多线程上下文中使用的能力(即两个线程可以共享对象,而不必担心一个线程会改变对象的状态,而另一个线程则可以担心)线程正在访问该状态)。通常,我们可以创建不可变类的实例比起初要实现的实例多:很多时候,我们出于习惯添加了getter或setter。


    结论


    我们创建的许多应用程序最终都可以运行,但是在许多应用程序中,我们引入了一些隐秘的问题,这些问题往往在最坏的情况下蔓延开来。在某些情况下,我们出于便利甚至出于习惯来做事情,并且在我们使用它们的情况下根本不在乎这些习惯用法是否实用(或安全)。在本文中,我们研究了其中三种最常见的做法,例如空返回值,对函数编程的亲和力,粗心的getter和setter以及一些实用的替代方法。尽管本文中的规则不应被视为绝对规则,但它们确实提供了对常见做法的罕见危险的一些见解,并可能有助于避免将来的繁琐错误。


    文章来源于网络,版权归原作者所有,内容为作者个人观点,文章仅供学习,如有侵权请联系客服删除,本站拥有对此声明的最终解释权。


     



  • Java
  • 举报文章

  • 收藏博客:

  • 分享至:
  • 添加评论

    请先登录再评论...

    登录

    评论列表(条评论)

    没有更多评论了