>  기사  >  Java  >  Java 상속, 다형성 및 클래스 재사용에 대한 자세한 소개 및 코드 예

Java 상속, 다형성 및 클래스 재사용에 대한 자세한 소개 및 코드 예

黄舟
黄舟원래의
2017-03-14 11:47:341992검색


추상:
이 기사에서는 Java 클래스 재사용을 객체 지향두 가지 주요 요소로 결합합니다. 기능상속과 다형성을 종합적으로 소개합니다. 먼저 상속의 본질과 의미를 소개하고, 클래스 재사용 측면에서 상속, 구성, 프록시 간의 유사점과 차이점을 살펴보았습니다. 다음으로 상속을 기반으로 한 다형성을 소개하고 그 구현 메커니즘과 구체적인 응용 프로그램을 소개했습니다. 또한 상속과 다형성에 대한 이해를 돕기 위해 final 키워드에 대해 포괄적으로 소개했습니다. 이를 바탕으로 Java에서 클래스의 로딩 및 초기화 순서를 소개했습니다. 마지막으로 객체 지향 설계에서 매우 중요한 세 가지 개념인 오버로딩, 덮어쓰기 및 숨기기에 대해 자세히 설명했습니다.


핵심사항:

  • 상속

  • 구성, 상속, 프록시

  • 다형성

  • 최종 키워드

  • 클래스 로딩 및 초기화 순서

  • 오버로딩, 오버라이딩 및 숨기기

1. 상속

상속은 모든 OOP 언어에서 없어서는 안 될 부분입니다. extend키워드 는 상속 관계를 나타냅니다. 클래스가 생성되면 항상 상속됩니다. 상속할 클래스를 명시적으로 지정하지 않으면 항상 루트 클래스 Object에서 암시적으로 상속됩니다. 두 클래스 사이에 상속 관계가 있는 경우 하위 클래스는 자동으로 상위 클래스의 메서드와 변수를 상속받습니다. 하위 클래스는 상위 클래스의 메서드와 변수를 직접 호출할 수 있습니다. Java에서는 단일 상속만 허용합니다 . 즉, 클래스는 최대 하나의 상위 클래스에서만 명시적으로 상속할 수 있습니다. 그러나 한 클래스는 여러 클래스에 의해 상속될 수 있습니다. 즉, 클래스는 여러 하위 클래스를 가질 수 있습니다. 또한 다음 사항에 특히 주의해야 합니다. 1.

멤버 변수의 상속

하위 클래스가 특정 클래스를 상속하는 경우 상위 클래스의 멤버 변수를 사용할 수 있지만 상위 클래스의 모든 멤버 변수가 완전히 상속되는 것은 아닙니다. 구체적인 원칙은 다음과 같습니다.

  • 하위 클래스는 상위 클래스의 공개 및 보호 멤버 변수 , 은 상위 클래스의 비공개 멤버 변수를 상속할 수 없지만 해당 getter/setter 메서드를 전달할 수 있습니다. 액세스를 위한 상위 클래스 ; 하위 클래스와 상위 클래스가 동일한 패키지에 있으면 하위 클래스가 상속할 수 있습니다. 그렇지 않으면 하위 클래스가 을 상속할 수 없습니다.

    하위 클래스가 클래스 멤버 변수를 상속할 수 있는 상위 클래스의 경우
  • 같은 이름의

    멤버 변수가 하위 클래스에 나타나면 Hidden 현상이 발생하게 되는데, 즉 하위 클래스에서 상위 클래스의 멤버 변수가 같은 이름을 가진 상위 클래스의 멤버 변수를 차단하게 됩니다. 하위 클래스의 상위 클래스에 있는 같은 이름의 멤버 변수에 액세스하려면 super 키워드를 사용하여 참조

    를 만들어야 합니다.
  • 2. 멤버 메서드 상속 마찬가지로 하위 클래스가 특정 클래스를 상속하는 경우 이를 사용할 수 있습니다. 하지만 하위 클래스가 상위 클래스의 모든 메서드를 완전히 상속하지는 않습니다. 구체적인 원칙은 다음과 같습니다.

    하위 클래스는 상위 클래스의

공개 및 보호 멤버 메서드 , 은 상위 클래스의

    비공개 멤버 메서드
  • 를 상속할 수 없습니다. 부모 클래스의 패키지 접근 멤버 방식 , 하위 클래스와 부모 클래스가 같은 패키지에 있으면 하위 클래스가 상속할 수 있고, 그렇지 않으면 하위 클래스는 상속될 수 없습니다.

  • 하위 클래스가 상속할 수 있는 상위 클래스 멤버 메소드의 경우 같은 이름의 멤버 메소드가 하위 클래스에 나타나면 이라고 합니다. 을 재정의합니다. 즉, 하위 클래스의 멤버 메서드는 동일한 이름을 가진 상위 클래스의 멤버 메서드를 재정의합니다. 하위 클래스의 상위 클래스에 있는 동일한 이름의 멤버 메서드에 액세스하려면 super 키워드를 참조용으로 사용해야 합니다.

프로그램 예:

class Person {    
public String gentle = "Father";
}public class Student extends Person {

    public String gentle = "Son";    
    public String print(){        
    return super.gentle;       
    // 在子类中访问父类中同名成员变
    }    public static void main(String[] args) throws ClassNotFoundException {
        Student student = new Student();
        System.out.println("##### " + student.gentle);
        Person p = student;
        System.out.println("***** " + p.gentle);    //隐藏:编译时决定,不会发生多态

        System.out.println("----- " + student.print());
        System.out.println("----- " + p.print());    //Error:Person 中未定义该方法
    }
}/* Output:
        ##### Son
        ***** Father
        ----- Father
 *///:~

 숨기기와 덮어쓰기는 다릅니다. 숨겨진 은 멤버 변수와 정적 메서드 의 경우 이고 재정의는 일반적인 방법에 대한 입니다.


3. 기본 클래스의 초기화 및 생성자

내보낸 클래스는 동일한 인터페이스를 가진 기본 클래스와 같다는 것을 알고 있습니다. 아마도 몇 가지 추가 메서드와 필드가 있는 새 클래스입니다. 그러나 상속은 단순히 기본 클래스의 인터페이스를 복사하는 것이 아닙니다. 파생 클래스 개체를 생성하면 해당 개체에는 기본 클래스의 하위 개체가 포함됩니다. 이 하위 개체는 기본 클래스를 사용하여 직접 생성한 개체와 동일합니다. 둘 사이의 차이점은 후자가 외부에서 오는 반면 기본 클래스의 하위 객체는 파생 클래스 객체 내부에 래핑된다는 것입니다.
 
따라서 은 기본 클래스 하위 개체의 올바른 초기화에 중요하며 Java도 이를 보장하기 위해 해당 메서드를 제공합니다. 파생 클래스는 다음을 수행해야 합니다. 초기화를 수행하기 위해 생성자에서 기본 클래스 생성자를 호출하며, 기본 클래스 생성자는 기본 클래스 초기화를 수행하는 데 필요한 모든 지식과 기능을 갖추고 있습니다. 기본 클래스에 기본 생성자가 포함된 경우 Java는 파생 클래스의 생성자에 기본 클래스의 기본 생성자에 대한 호출을 자동으로 삽입합니다. 왜냐하면 컴파일러는 전달할 매개변수를 고려할 필요가 없기 때문입니다. 그러나 상위 클래스에 기본 생성자가 없거나 파생 클래스가 매개 변수를 사용하여 상위 클래스 생성자를 호출하려는 경우 super 키워드를 사용하여 파생 클래스의 생성자에서 이를 명시적으로 호출해야 합니다. 해당 기본 클래스 생성자 및 호출 문은 파생 클래스 생성자의 첫 번째 문이어야 합니다.


2. 조합, 상속, 프록시

Java에서는 조합, 상속, 프록시 모두 재사용 코드를 구현할 수 있습니다.

(1) 구성(has-a)

 기존 클래스를 새 클래스에 추가하면 객체를 만들 수 있습니다. 결합됩니다. 즉, 새로운 클래스는 기존 클래스의 객체로 구성됩니다. 이 기술은 일반적으로 인터페이스가 아닌 기존 클래스의 기능을 새 클래스에서 사용하려는 경우에 사용됩니다. 즉, 필요한 기능을 수행할 수 있도록 새 클래스에 객체를 삽입하지만 새 클래스의 사용자는 새 클래스에 대해 정의된 인터페이스만 볼 수 있습니다. 임베디드 객체의 인터페이스.


(2) 상속(is-a)

상속을 통해 기존 클래스의 유형을 상속받을 수 있습니다. 새로운 수업. 즉, 기존 클래스의 형태를 취하고 그 안에 새로운 코드를 추가합니다. 일반적으로 이는 일반 수업을 듣고 특별한 요구 사항에 맞게 전문 수업을 진행한다는 의미입니다. 기본적으로 합성과 상속 모두 하위 객체를 새 클래스에 배치하는 것을 허용합니다. 합성은 이를 명시적으로 수행하는 반면 상속은 암시적으로 수행합니다.


(3) Proxy(상속과 합성의 황금률: 기존 클래스의 기능을 합성처럼 사용하면서 기존 클래스의 상속 인터페이스처럼 사용)

 프록시는 상속과 구성의 황금중간인데, 자바는 이를 직접적으로 지원하지 않는다. 프록시에서는 생성되는 클래스에 멤버 개체를 배치하지만(구성과 유사) 동시에 해당 멤버 개체의 인터페이스/메서드를 새 클래스에 노출합니다(상속과 유사).
 우주선으로 이를 수행하는 한 가지 방법은 상속을 사용하는 것입니다:

public class SpaceShip extends SpaceShipControls { 
    private String name; 
    public SpaceShip(String name) { this.name = name; } 
    public String toString() { return name; } 
    public static void main(String[] args) { 
        SpaceShip protector = new SpaceShip("NSEA Protector"); 
        protector.forward(100); 
    } 
}

  然而,SpaceShip 并不是真正的 SpaceShipControls 类型,即便你可以“告诉” SpaceShip 向前运动(forward())。更准确的说,SpaceShip 包含 SpaceShipControls ,与此同时, SpaceShipControls 的所有方法在 SpaceShip 中都暴露出来。 代理(SpaceShip 的运动行为由 SpaceShipControls 代理完成) 正好可以解决这种问题:

// SpaceShip 的行为由 SpaceShipControls 代理完成public class SpaceShipDelegation { 
    private String name; 
    private SpaceShipControls controls = new SpaceShipControls(); 

    public SpaceShipDelegation(String name) { 
        this.name = name; 
    } 

    // 代理方法: 
    public void back(int velocity) { 
        controls.back(velocity); 
    } 
    public void down(int velocity) { 
        controls.down(velocity); 
    } 
    public void forward(int velocity) { 
        controls.forward(velocity); 
    } 
    public void left(int velocity) { 
        controls.left(velocity); 
    } 
    public void right(int velocity) { 
        controls.right(velocity); 
    } 
    public void turboBoost() { 
        controls.turboBoost(); 
    } 
    public void up(int velocity) { 
        controls.up(velocity); 
    } 

    public static void main(String[] args) { 
        SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector"); 
        protector.forward(100); 
    } 
}

  实际上,我们使用代理时可以拥有更多的控制力,因为我们可以选择只提供在成员对象中方法的某个子集。


三. final关键字

  许多编程语言都需要某种方法来向编译器告知一块数据是恒定不变的。有时,数据的恒定不变是很有用的,比如:

  • 一个永不改变的编译时常量

  • 一个在运行时被初始化的值,而你不希望它被改变。

      对于编译期常量这种情况,编译器可以将该常量值带入任何可能用到它的计算式中,也即是说,可以在编译时执行计算式,这减轻了一些运行时负担。在Java中,这类常量必须满足两个条件:

  • 是基本类型,并且用final修饰;

  • 在对这个常量进行定义的时候,必须对其进行赋值。

      此外,当用final修饰对象引用时,final使其引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它指向另一个对象。然而,对象本身是可以被修改的,这同样适用于数组,因为它也是对象。

      特别需要注意的是,我们不能因为某数据是final的,就认为在编译时就可以知道它的值。例如:

public class Test { 
    final int i4 = rand.nextInt(20);
}

1、空白final

  Java允许生成 空白final , 即:声明final但又未给定初值的域。但无论什么情况,编译器都会确保空白final在使用前被初始化。但是,空白final在关键字final的使用上提供了更大的灵活性: 一个类中的 final域 就可以做到根据对象而有所不同,却又保持其恒定不变的特性。例如,
         blank final.png-58.9kB
         
  必须在域的定义处或者每个构造器中使用表达式对final进行赋值,这正是 final域 在使用前总是被初始化的原因所在。


2、final参数

  final参数 主要应用于局部内部类和匿名内部类中,更多详细介绍请移步我的另一篇文章:Java 内部类综述。


3、final方法

  final关键字作用域方法时,用于锁定方法,以防任何继承类修改它的含义。这是出于设计的考虑:想要确保在继承中使方法行为保持不变,并且不会被覆盖。

  对于成员方法,只有在明确禁止覆盖时,才将方法设为final的。


4、final类

 클래스를 final로 정의한다는 것은 해당 클래스를 상속할 의도가 없으며 다른 사람이 상속하도록 허용하지 않음을 나타냅니다. 즉, 어떤 이유로든 이 클래스의 디자인을 변경할 필요가 없거나 보안 이유로 인해 변경하지 않으려는 것입니다. 서브클래스 종류.
 
최종수업 분야는 실제 상황에 따라 최종수업 여부를 선택할 수 있다는 점 참고하시기 바랍니다. final로 정의되었는지 여부에 관계없이 final로 정의된 필드에는 동일한 규칙이 적용됩니다. 그러나 final 클래스는 상속을 금지하므로 final 클래스의 모든 메소드는 재정의될 수 없으므로 암묵적으로 final로 지정됩니다. 최종 클래스의 메서드에 최종 수정 사항을 추가할 수 있지만 이것이 아무런 의미를 추가하지는 않습니다.


5. final 및 private

 클래스의 모든 private 메소드는 암묵적으로 final로 지정됩니다. private 메소드는 접근이 불가능하기 때문에 재정의가 불가능합니다. 비공개 메서드에 최종 수정 사항을 추가할 수 있지만 이것이 메서드에 추가적인 의미를 추가하지는 않습니다.
 
재정의는 메서드가 기본 클래스 인터페이스 의 일부인 경우에만 나타납니다. 메소드가 비공개인 경우 이는 기본 클래스 인터페이스의 일부가 아니라 클래스에 숨겨진 일부 프로그램 코드일 뿐입니다. 그러나 내보낸 클래스에 동일한 이름으로 비공개 메서드가 생성된 경우에는 이때 해당 메서드를 덮어쓰지 않고 새 메서드만 생성합니다. private 메소드는 접근이 불가능하고 효과적으로 숨겨질 수 있기 때문에 그것이 속한 클래스의 조직 구조로 인해 존재한다는 점 외에는 다른 상황에서는 고려할 필요가 없습니다.


6. final 및 static 🎜>

  • 을 변경할 수 있으며,

    은 멤버 변수와 멤버 메서드만 수정할 수 있습니다. 정적 최종 필드

    는 변경이 불가능한 저장 공간만 차지하며, 초기화는 선언시에만 가능합니다
  • .

    최종이므로 기본값이 없고 정적이므로 클래스가 인스턴스화되지 않을 때 값이 할당되므로 선언되어야만 초기화할 수 있습니다. IV. 다형성 상속을 통해 객체를 자체 유형 또는 기본으로 처리할 수 있다는 것을 알고 있습니다. 동일한 코드가 이러한 다양한 유형에서 아무런 차이 없이 실행될 수 있도록 유형이 처리됩니다. 그중에서도 다형성 메서드 호출을 사용하면 해당 유형이 동일한 기본 클래스에서 파생되는 한 한 유형이 다른 유사한 유형과 다르게 동작할 수 있습니다. 그래서

  • 다형성의 역할은 크게 두 가지 측면으로 반영됩니다.

분리를 통한 다형성 무엇 수행 방법 및 인터페이스와 구현을 다른 관점에서 분리하여 변경된 항목과 변경되지 않은 항목을 분리합니다. 타입 간의 결합 관계 제거 (마찬가지로 Java에서는 클래스나 메소드와 사용되는 유형 간의 결합 관계를 제거하기 위해 제네릭도 사용됩니다.)

  • 1. 구현 메커니즘

    메서드 재정의가 다형성을 매우 잘 구현한다는 것을 알고 있지만 기본 클래스 참조를 사용하여 재정의 메서드를 호출할 때 어떤 메서드를 올바르게 호출해야 할까요?

    메서드 호출을 동일한 메소드 본문과 연결하는 것을 바인딩이라고 합니다. 프로그램 실행 전에 바인딩을 하는 것을 얼리 바인딩이라고 합니다. 그러나 분명히 이 메커니즘은 위의 문제를 해결할 수 없습니다. 왜냐하면 컴파일러는 컴파일 타임에 위의 기본 클래스 참조가 어떤 개체를 가리키는지 알 수 없기 때문입니다. 해결책은 런타임 바인딩(동적 바인딩/런타임 바인딩): 런타임 시 개체의 특정 유형에 따라 바인딩하는 것입니다.

    실제로 Java에서는 정적 메소드와 최종 메소드(프라이빗 메소드가 최종 메소드임)를 제외한 모든 메소드가 런타임에 바인딩됩니다. 이런 방식으로 메서드가 final로 선언된 후에 다른 사람이 메서드를 재정의하는 것을 방지할 수 있지만 더 중요한 것은 이렇게 하면 동적 바인딩을 효과적으로 끌 수 있습니다. 즉, 컴파일러에 그렇게 한다고 알릴 수 있습니다. 최종 메서드 호출을 위해 보다 효율적인 코드를 생성하기 위해 동적 바인딩을 수정할 필요가 없습니다.

    동적 바인딩 메커니즘을 기반으로 기본 클래스만 처리하는 코드를 작성할 수 있으며 이러한 코드는 내보낸 모든 클래스에 대해 올바르게 실행될 수 있습니다. 즉, 개체에 메시지를 보내고 개체가 무엇을 할지 결정하도록 합니다.


    2. 다운캐스팅 및 런타임 유형 식별

    업캐스팅은 특정 유형 정보를 잃어버리므로, 획득도 가능해야 한다고 생각할 수 있습니다. 다운캐스팅을 통해 정보를 입력합니다. 그러나 기본 클래스가 파생 클래스보다 더 큰 인터페이스를 갖지 않기 때문에 업캐스팅이 안전하다는 것을 알고 있습니다. 따라서 기본 클래스 인터페이스를 통해 보내는 메시지는 허용되지만 하향 변환에 대해서는 보장할 수 없습니다.

    이 문제를 해결하려면, 경솔하게 잘못된 유형으로 캐스팅한 후 객체가 받아들일 수 없는 메시지를 보내는 일이 없도록 하향 캐스팅의 정확성을 보장할 수 있는 방법이 있어야 합니다. Java에서는 RTTI(런타임 유형 식별) 메커니즘이 이 문제를 처리할 수 있으므로 Java의 모든 변환이 검사됩니다. 따라서 괄호로 묶인 일반적인 유형 변환을 수행하더라도 런타임에 들어갈 때 실제로 우리가 원하는 유형인지 확인됩니다. 그렇지 않은 경우 유형 변환 예외인 ClassCastException이 발생합니다.


    3. 다형성 적용 예시

    즉, 먼저 프로그램에 나타나는 순서대로 상위 클래스의 정적 멤버 변수와 정적 코드 블록을 초기화합니다. 프로그램에 나타나는 순서대로 하위 클래스를 초기화합니다. 두 번째로 상위 클래스의 일반 멤버 변수와 코드 블록을 초기화한 다음 상위 클래스의
    생성 메서드

    를 실행합니다. 하위 클래스의 멤버 변수와 코드 블록을 검색한 다음 하위 클래스의 생성 메서드를 실행합니다.

    다음 프로그램을 통해 설명하겠습니다.

    class SuperClass {    private static String STR = "Super Class Static Variable";    static {
            System.out.println("Super Class Static Block:" + STR);
        }    public SuperClass() {
            System.out.println("Super Class Constructor Method");
        }
    
        {
            System.out.println("Super Class Block");
        }
    
    }public class ObjectInit extends SuperClass {
        private static String STR = "Class Static Variable";    static {
            System.out.println("Class Static Block:" + STR);
        }    public ObjectInit() {
            System.out.println("Constructor Method");
        }
    
        {
            System.out.println("Class Block");
        }    public static void main(String[] args) {        @SuppressWarnings("unused")
            ObjectInit a = new ObjectInit();
        }
    }/* Output:
            Super Class Static Block:Super Class Static Variable
            Class Static Block:Class Static Variable
            Super Class Block
            Super Class Constructor Method
            Class Block
            Constructor Method
     *///:~

      在运行该程序时,所发生的第一件事就是试图访问 ObjectInit.main() 方法(一个static方法),于是加载器开始启动并加载 ObjectInit类 。在对其加载时,编译器注意到它有一个基类(这由关键字extends得知),于是先进行加载其基类。如果该基类还有其自身的基类,那么先加载这个父父基类,如此类推(本例中是先加载 Object类 ,再加载 SuperClass类 ,最后加载 ObjectInit类 )。接下来,根基类中的 static域 和 static代码块 会被执行,然后是下一个导出类,以此类推这种方式很重要,因为导出类的static初始化可能会依赖于基类成员能否被正确初始化。到此为止,所有的类都已加载完毕,对象就可以创建了。首先,初始化根基类所有的普通成员变量和代码块,然后执行根基类构造器以便创建一个基对象,然后是下一个导出类,依次类推,直到初始化完成。


    六. 重载、覆盖与隐藏

    1、重载与覆盖

    (1) 定义与区别

      重载如果在一个类中定义了多个同名的方法,但它们有不同的参数(包含三方面:参数个数、参数类型和参数顺序),则称为方法的重载。其中,不能通过访问权限、返回类型和抛出异常进行重载。
       
      覆盖子类中定义的某个方法与其父类中某个方法具有相同的方法签名(包含相同的名称和参数列表),则称为方法的覆盖。子类对象使用这个方法时,将调用该方法在子类中的定义,对它而言,父类中该方法的定义被屏蔽了。

      总的来说,重载和覆盖是Java多态性的不同表现。前者是一个类中多态性的一种表现,后者是父类与子类之间多态性的一种表现。


    (2) 实现机制

      重载是一种参数多态机制,即通过方法参数的差异实现多态机制。并且,其属于一种 静态绑定机制,在编译时已经知道具体执行哪个方法。
       
      覆盖是一种动态绑定的多态机制。即,在父类与子类中具有相同签名的方法具有不同的具体实现,至于最终执行哪个方法 根据运行时的实际情况而定。


    (3) 总结

      我们应该注意以下几点:

    • final 方法不能被覆盖;

    • 子类不能覆盖父类的private方法,否则,只是在子类中定义了一个与父类重名的全新的方法,而不会有任何覆盖效果。

        其他需要注意的地方如下图所示:

               Java 상속, 다형성 및 클래스 재사용에 대한 자세한 소개 및 코드 예


    2、覆盖与隐藏

    (1) 定义

    覆盖:指 运行时系统调用当前对象引用 运行时类型 中定义的方法 ,属于 运行期绑定。

    隐藏:指运行时系统调用当前对象引用 编译时类型 中定义的方法,即 被声明或者转换为什么类型就调用对应类型中的方法或变量,属于编译期绑定。


    (2) 范围

    재정의: 인스턴스 메서드에만 해당
    숨기기 : 정적 메소드 및 멤버 변수에만 해당됩니다.


    (3) 요약

    • 하위 클래스의 인스턴스 메서드는 상위 클래스의 정적 메서드를 숨길 수 없습니다. 마찬가지로 하위 클래스의 정적 메서드는 상위 클래스의 인스턴스 메서드를 재정의할 수 없습니다. 그렇지 않으면 컴파일 오류가 발생합니다. 🎜>

    • 정적 멤버와 인스턴스 멤버 모두 하위 클래스에서 이름이 같은 멤버 변수에 의해 숨겨질 수 있습니다.

      다음 프로그램 예제에서는 오버로딩, 재정의 및 잘 숨기기의 세 가지 개념을 설명합니다.    
      Java 상속, 다형성 및 클래스 재사용에 대한 자세한 소개 및 코드 예

위 내용은 Java 상속, 다형성 및 클래스 재사용에 대한 자세한 소개 및 코드 예의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.