Heim > Java > javaLernprogramm > Was sind die Objektmethoden in Java?

Was sind die Objektmethoden in Java?

coldplay.xixi
Freigeben: 2020-10-29 16:20:57
Original
13411 Leute haben es durchsucht

Zu den Objektmethoden in Java gehören: 1. [getClass()] ist eine öffentliche Methode; 2. [hashCode()] ist eine öffentliche Methode und kann direkt über das Objekt aufgerufen werden. 3. [equals()] wird verwendet zum Vergleich Ob das aktuelle Objekt und das Zielobjekt gleich sind.

Was sind die Objektmethoden in Java?

Verwandte kostenlose Lernempfehlungen: Java-Grundlagen-Tutorial

Objektmethoden in Java sind:

1. Einführung

Object ist die Basisklasse von allen Klassen und ist Das Ganze Die oberste Vererbungsstruktur der Klasse ist auch die abstrakteste Klasse. Jeder verwendet jeden Tag toString(), equal(), hashCode(), wait(), notify(), getClass() und andere Methoden. Vielleicht ist ihnen nicht klar, dass es sich um Methoden von Object handelt, und sie schauen sich auch nicht an, was andere Methoden, die Object hat. Und überlegen Sie, warum diese Methoden in Object platziert werden sollten. In diesem Artikel werden die spezifischen Funktionen jeder Methode, die Umschreibungsregeln und einige meiner eigenen Erkenntnisse erläutert.

2. Detaillierte Erläuterung der Objektmethoden

Objekt enthält: registerNatives(), getClass(), hashCode(), equal(), clone(), toString(), notify(), notifyAll(), wait(long) Insgesamt gibt es zwölf Methoden: wait(long,int), wait() und finalize(). Diese Reihenfolge ist in der Reihenfolge aufgeführt, in der die Methoden in der Object-Klasse definiert sind. Ich werde sie auch in dieser Reihenfolge unten erklären.

1.1, registerNatives()

public class Object {
    private static native void registerNatives();
    static {
        registerNatives();
    }
}
Nach dem Login kopieren

Was zum Teufel? Hahaha, ich habe gerade diese Methode gesehen und war verwirrt. Aus dem Namen geht hervor, dass es sich bei dieser Methode um eine native Methode handelt (eine native Methode, die von der JVM implementiert wird und die unterste Ebene in C/C++ implementiert ist). Bei wem sollten Sie sie registrieren? Natürlich liegt es an der JVM. Wenn ein Programm eine native Methode aufruft, kann die JVM diese zugrunde liegenden Methoden finden und aufrufen.

Die native Methode in Object und verwenden Sie registerNatives(), um sich bei der JVM zu registrieren. (Das gehört zur Kategorie JNI. Ich weiß es noch nicht. Wenn Sie interessiert sind, können Sie es selbst ausprobieren.)

static JNINativeMethod methods[] = {
    {"hashCode",    "()I",                    (void *)&JVM_IHashCode},
    {"wait",        "(J)V",                   (void *)&JVM_MonitorWait},
    {"notify",      "()V",                    (void *)&JVM_MonitorNotify},
    {"notifyAll",   "()V",                    (void *)&JVM_MonitorNotifyAll},
    {"clone",       "()Ljava/lang/Object;",   (void *)&JVM_Clone},
};
Nach dem Login kopieren

Warum sollten wir statische Methoden verwenden und sie in statische Blöcke einfügen?

Wir wissen, dass bei der Initialisierung einer Klasse die Klassenvariablen von der übergeordneten Klasse zu dieser Klasse sowie die Klassenvariablen und -methoden im Klasseninitialisierungsblock in der Reihenfolge ihrer Definition platziert werden um sicherzustellen, dass die Klassenvariablen der übergeordneten Klasse Variablen und Methoden vor Unterklassen initialisiert werden müssen. Wenn eine Unterklasse daher die entsprechende native Methode aufruft, z. B. bei der Berechnung von HashCode, kann sie garantiert die native Methode der JVM aufrufen.

1.2, getClass()

public final native Class getClass(): Dies ist eine öffentliche Methode, die wir direkt über das Objekt aufrufen können.

Die erste Phase des Klassenladens besteht darin, die .class-Datei in den Speicher zu laden und ein java.lang.Class-Objekt zu generieren. Die Methode getClass() dient dazu, dieses Objekt abzurufen, bei dem es sich um eine Sammlung aller Informationen über die Laufzeitklasse des Objekts der aktuellen Klasse handelt. Diese Methode ist eine der drei Reflexionsmethoden.

1.2.1. Drei Arten der Reflexion:

Klassenname.Klasse;

Class.forName();
class extends ObjectTest {
    private void privateTest(String str) {
        System.out.println(str);
    }
    public void say(String str) {
        System.out.println(str);
    }
}
public class ObjectTest {
    public static void main(String[] args) throws Exception {
        ObjectTest  = new ();
        //获取对象运行的Class对象
        Class<? extends ObjectTest> aClass = .getClass();
        System.out.println(aClass);
        //getDeclaredMethod这个方法可以获取所有的方法,包括私有方法
        Method privateTest = aClass.getDeclaredMethod("privateTest", String.class);
        //取消java访问修饰符限制。
        privateTest.setAccessible(true);
        privateTest.invoke(aClass.newInstance(), "private method test");
        //getMethod只能获取public方法
        Method say = aClass.getMethod("say", String.class);
        say.invoke(aClass.newInstance(), "Hello World");
    }
}
//输出结果:
//class test.
//private method test
//Hello World
Nach dem Login kopieren

Reflexion kann beim Schreiben eine dynamische Sprache sein Code, weisen Sie ein untergeordnetes Objekt einer Referenz der übergeordneten Klasse zu, und alle Informationen des Laufzeitobjekts können durch Reflexion zur Laufzeit abgerufen werden, was eine Manifestation des Polymorphismus ist. Es gibt noch viel Wissen über die Reflexion, daher werde ich hier nicht näher darauf eingehen.

1.3, hashCode()

public native int hashCode(); Dies ist eine öffentliche Methode, daher können Unterklassen sie überschreiben. Diese Methode gibt den HashCode-Wert des aktuellen Objekts zurück, bei dem es sich um eine Zahl im Ganzzahlbereich (-2^31 ~ 2^31 - 1) handelt.

Es gibt die folgenden Einschränkungen für hashCode:

Wenn während der Ausführung einer Java-Anwendung die hashCode-Methode mehrmals für dasselbe Objekt aufgerufen wird, muss dieselbe Ganzzahl konsistent zurückgegeben werden, vorausgesetzt, dass die Informationen verwendet werden, mit denen die Objekte verglichen werden equal wurde nicht geändert;

Wenn die x.equals(y)-Methode zweier Objekte true zurückgibt, muss der HashCode der beiden Objekte x und y gleich sein.

Wenn die x.equals(y)-Methode zweier Objekte false zurückgibt, kann der HashCode der beiden Objekte x und y gleich oder unterschiedlich sein. Allerdings kann die Generierung unterschiedlicher ganzzahliger Ergebnisse für ungleiche Objekte die Leistung der Hash-Tabelle verbessern.

Der Standard-HashCode ist der aus der Speicheradresse konvertierte Hash-Wert. Nach dem Umschreiben handelt es sich um eine benutzerdefinierte Berechnungsmethode, die auch über System.identityHashCode(Object) zurückgegeben werden kann.

public class HashCodeTest {
    private int age;
    private String name;
    @Override
    public int hashCode() {
        Object[] a = Stream.of(age, name).toArray();
        int result = 1;
        for (Object element : a) {
            result = 31 * result + (element == null ? 0 : element.hashCode());
        }
        return result;
    }
}
Nach dem Login kopieren

Es wird empfohlen, die Methode Objects.hash(Object… Values) zu verwenden. Ich glaube, wenn Sie sich den Quellcode ansehen, werden Sie feststellen, dass 31 als grundlegender Multiplikator zur Berechnung des Hash-Codes verwendet wird. Ich stimme zu und verstehe Ergebnis * 31 = (Ergebnis<<5) - Ergebnis. Die unterste Ebene der JVM kann Bitoperationen automatisch optimieren, was sehr effizient ist und da der von 31 berechnete HashCode weniger Konflikte aufweist, ist dies förderlich für die Verteilung von Hash-Bucket-Bits.

1.4, equal()

public boolean equal(Object obj); wird verwendet, um zu vergleichen, ob das aktuelle Objekt und das Zielobjekt gleich sind. Standardmäßig wird verglichen, ob die Referenz auf dasselbe Objekt verweist. Es ist eine öffentliche Methode und kann von Unterklassen überschrieben werden.

public class Object{
    public boolean equals(Object obj) {
        return (this == obj);
    }
}
Nach dem Login kopieren

Warum müssen Sie die Methode „equals“ überschreiben?

因为如果不重写equals方法,当将自定义对象放到map或者set中时;如果这时两个对象的hashCode相同,就会调用equals方法进行比较,这个时候会调用Object中默认的equals方法,而默认的equals方法只是比较了两个对象的引用是否指向了同一个对象,显然大多数时候都不会指向,这样就会将重复对象存入map或者set中。这就破坏了map与set不能存储重复对象的特性,会造成内存溢出。

重写equals方法的几条约定:

自反性:即x.equals(x)返回true,x不为null;

对称性:即x.equals(y)与y.equals(x)的结果相同,x与y不为null;

传递性:即x.equals(y)结果为true, y.equals(z)结果为true,则x.equals(z)结果也必须为true;

一致性:即x.equals(y)返回true或false,在未更改equals方法使用的参数条件下,多次调用返回的结果也必须一致。x与y不为null。

如果x不为null, x.equals(null)返回false。

我们根据上述规则来重写equals方法。

public class EqualsTest{
    private int age;
    private String name;
    //省略get、set、构造函数等
     @Override
    public boolean equals(Object o) {
        //先判断是否为同一对象
        if (this == o) {
            return true;
        }
        //再判断目标对象是否是当前类及子类的实例对象
        //注意:instanceof包括了判断为null的情况,如果o为null,则返回false
        if (!(o instanceof )) {
            return false;
        }
         that = () o;
        return age == that.age &&
                Objects.equals(name, that.name);
    }
     public static void main(String[] args) throws Exception {
         EqualsTest1 equalsTest1 = new EqualsTest1(23, "9龙");
        EqualsTest1 equalsTest12 = new EqualsTest1(23, "9龙");
        EqualsTest1 equalsTest13 = new EqualsTest1(23, "9龙");
        System.out.println("-----------自反性----------");
        System.out.println(equalsTest1.equals(equalsTest1));
        System.out.println("-----------对称性----------");
        System.out.println(equalsTest12.equals(equalsTest1));
        System.out.println(equalsTest1.equals(equalsTest12));
        System.out.println("-----------传递性----------");
        System.out.println(equalsTest1.equals(equalsTest12));
        System.out.println(equalsTest12.equals(equalsTest13));
        System.out.println(equalsTest1.equals(equalsTest13));
        System.out.println("-----------一致性----------");
        System.out.println(equalsTest1.equals(equalsTest12));
        System.out.println(equalsTest1.equals(equalsTest12));
        System.out.println("-----目标对象为null情况----");
        System.out.println(equalsTest1.equals(null));
    }
}
//输出结果
//-----------自反性----------
//true
//-----------对称性----------
//true
//true
//-----------传递性----------
//true
//true
//true
//-----------一致性----------
//true
//true
//-----目标对象为null情况----
//false
Nach dem Login kopieren

从以上输出结果验证了我们的重写规定是正确的。

注意:instanceof 关键字已经帮我们做了目标对象为null返回false,我们就不用再去显示判断了。

建议equals及hashCode两个方法,需要重写时,两个都要重写,一般都是将自定义对象放至Set中,或者Map中的key时,需要重写这两个方法。

1.4、clone()

protected native Object clone() throws CloneNotSupportedException;

此方法返回当前对象的一个副本。

这是一个protected方法,提供给子类重写。但需要实现Cloneable接口,这是一个标记接口,如果没有实现,当调用object.clone()方法,会抛出CloneNotSupportedException。

public class CloneTest implements Cloneable {
    private int age;
    private String name;
    //省略get、set、构造函数等
    @Override
    protected CloneTest clone() throws CloneNotSupportedException {
        return (CloneTest) super.clone();
    }
    public static void main(String[] args) throws CloneNotSupportedException {
        CloneTest cloneTest = new CloneTest(23, "9龙");
        CloneTest clone = cloneTest.clone();
        System.out.println(clone == cloneTest);
        System.out.println(cloneTest.getAge()==clone.getAge());
        System.out.println(cloneTest.getName()==clone.getName());
    }
}
//输出结果
//false
//true
//true
Nach dem Login kopieren

从输出我们看见,clone的对象是一个新的对象;但原对象与clone对象的String类型的name却是同一个引用,这表明,super.clone方法对成员变量如果是引用类型,进行是浅拷贝。

那什么是浅拷贝?对应的深拷贝?

浅拷贝:拷贝的是引用。

深拷贝:新开辟内存空间,进行值拷贝。

那如果我们要进行深拷贝怎么办呢?看下面的例子。

class Person implements Cloneable{
    private int age;
    private String name;
     //省略get、set、构造函数等
     @Override
    protected Person clone() throws CloneNotSupportedException {
        Person person = (Person) super.clone();
        //name通过new开辟内存空间
        person.name = new String(name);
        return person;
   }
}
public class CloneTest implements Cloneable {
    private int age;
    private String name;
    //增加了person成员变量
    private Person person;
    //省略get、set、构造函数等
    @Override
    protected CloneTest clone() throws CloneNotSupportedException {
        CloneTest clone = (CloneTest) super.clone();
        clone.person = person.clone();
        return clone;
    }
    public static void main(String[] args) throws CloneNotSupportedException {
       CloneTest cloneTest = new CloneTest(23, "9龙");
        Person person = new Person(22, "路飞");
        cloneTest.setPerson(person);
        CloneTest clone = cloneTest.clone();
        System.out.println(clone == cloneTest);
        System.out.println(cloneTest.getAge() == clone.getAge());
        System.out.println(cloneTest.getName() == clone.getName());
        Person clonePerson = clone.getPerson();
        System.out.println(person == clonePerson);
        System.out.println(person.getName() == clonePerson.getName());
    }
}
//输出结果
//false
//true
//true
//false
//false
Nach dem Login kopieren

可以看到,即使成员变量是引用类型,我们也实现了深拷贝。如果成员变量是引用类型,想实现深拷贝,则成员变量也要实现Cloneable接口,重写clone方法。

1.5、toString()

public String toString();这是一个public方法,子类可重写,建议所有子类都重写toString方法,默认的toString方法,只是将当前类的全限定性类名+@+十六进制的hashCode值。

public class Object{
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
}
Nach dem Login kopieren

我们思考一下为什么需要toString方法?

我这么理解的,返回当前对象的字符串表示,可以将其打印方便查看对象的信息,方便记录日志信息提供调试。

我们可以选择需要表示的重要信息重写到toString方法中。为什么Object的toString方法只记录类名跟内存地址呢?因为Object没有其他信息了,哈哈哈。

1.6、wait()/ wait(long)/ waite(long,int)

这三个方法是用来线程间通信用的,作用是阻塞当前线程,等待其他线程调用notify()/notifyAll()方法将其唤醒。这些方法都是public final的,不可被重写。

注意:

此方法只能在当前线程获取到对象的锁监视器之后才能调用,否则会抛出IllegalMonitorStateException异常。

调用wait方法,线程会将锁监视器进行释放;而Thread.sleep,Thread.yield()并不会释放锁。

wait方法会一直阻塞,直到其他线程调用当前对象的notify()/notifyAll()方法将其唤醒;而wait(long)是等待给定超时时间内(单位毫秒),如果还没有调用notify()/nofiyAll()会自动唤醒;waite(long,int)如果第二个参数大于0并且小于999999,则第一个参数+1作为超时时间;

 public final void wait() throws InterruptedException {
        wait(0);
    } 
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
        if (nanos > 0) {
            timeout++;
        }
        wait(timeout);
    }
Nach dem Login kopieren

1.7、notify()/notifyAll()

前面说了,如果当前线程获得了当前对象锁,调用wait方法,将锁释放并阻塞;这时另一个线程获取到了此对象锁,并调用此对象的notify()/notifyAll()方法将之前的线程唤醒。这些方法都是public final的,不可被重写。

public final native void notify(); 随机唤醒之前在当前对象上调用wait方法的一个线程

public final native void notifyAll(); 唤醒所有之前在当前对象上调用wait方法的线程

下面我们使用wait()、notify()展示线程间通信。假设9龙有一个账户,只要9龙一发工资,就被女朋友给取走了。

//账户
public class Account {
    private String accountNo;
    private double balance;
    private boolean flag = false;
    public Account() {
    }
    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }
    /**
     * 取钱方法
     *
     * @param drawAmount 取款金额
     */
    public synchronized void draw(double drawAmount) {
        try {
            if (!flag) {
                //如果flag为false,表明账户还没有存入钱,取钱方法阻塞
                wait();
            } else {
                //执行取钱操作
                System.out.println(Thread.currentThread().getName() + " 取钱" + drawAmount);
                balance -= drawAmount;
                //标识账户已没钱
                flag = false;
                //唤醒其他线程
                notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void deposit(double depositAmount) {
        try {
            if (flag) {
                //如果flag为true,表明账户已经存入钱,取钱方法阻塞
                wait();
            } else {
                //存钱操作
                System.out.println(Thread.currentThread().getName() + " 存钱" + depositAmount);
                balance += depositAmount;
                //标识账户已存入钱
                flag = true;
                //唤醒其他线程
                notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
//取钱者
public class DrawThread extends Thread {
    private Account account;
    private double drawAmount;
    public DrawThread(String name, Account account, double drawAmount) {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }
    @Override
    public void run() {
        //循环6次取钱
        for (int i = 0; i < 6; i++) {
            account.draw(drawAmount);
        }
    }
}
//存钱者
public class DepositThread extends Thread {
    private Account account;
    private double depositAmount;
    public DepositThread(String name, Account account, double depositAmount) {
        super(name);
        this.account = account;
        this.depositAmount = depositAmount;
    }
    @Override
    public void run() {
        //循环6次存钱操作
        for (int i = 0; i < 6; i++) {
            account.deposit(depositAmount);
        }
    }
}
//测试
public class DrawTest {
    public static void main(String[] args) {
        Account brady = new Account("9龙", 0);
        new DrawThread("女票", brady, 10).start();
        new DepositThread("公司", brady, 10).start();
    }
}
//输出结果
//公司 存钱10.0
//女票 取钱10.0
//公司 存钱10.0
//女票 取钱10.0
//公司 存钱10.0
//女票 取钱10.0
Nach dem Login kopieren

例子中我们通过一个boolean变量来判断账户是否有钱,当取钱线程来判断如果账户没钱,就会调用wait方法将此线程进行阻塞;这时候存钱线程判断到账户没钱, 就会将钱存入账户,并且调用notify()方法通知被阻塞的线程,并更改标志;取钱线程收到通知后,再次获取到cpu的调度就可以进行取钱。反复更改标志,通过调用wait与notify()进行线程间通信。实际中我们会时候生产者消费者队列会更简单。

注意:调用notify()后,阻塞线程被唤醒,可以参与锁的竞争,但可能调用notify()方法的线程还要继续做其他事,锁并未释放,所以我们看到的结果是,无论notify()是在方法一开始调用,还是最后调用,阻塞线程都要等待当前线程结束才能开始。

为什么wait()/notify()方法要放到Object中呢?

因为每个对象都可以成为锁监视器对象,所以放到Object中,可以直接使用。

1.8、finalize()

protected void finalize() throws Throwable ;

此方法是在垃圾回收之前,JVM会调用此方法来清理资源。此方法可能会将对象重新置为可达状态,导致JVM无法进行垃圾回收。

我们知道java相对于C++很大的优势是程序员不用手动管理内存,内存由jvm管理;如果我们的引用对象在堆中没有引用指向他们时,当内存不足时,JVM会自动将这些对象进行回收释放内存,这就是我们常说的垃圾回收。但垃圾回收没有讲述的这么简单。

finalize()方法具有如下4个特点:

永远不要主动调用某个对象的finalize()方法,该方法由垃圾回收机制自己调用;

finalize()何时被调用,是否被调用具有不确定性;

当JVM执行可恢复对象的finalize()可能会将此对象重新变为可达状态;

当JVM执行finalize()方法时出现异常,垃圾回收机制不会报告异常,程序继续执行。

public class FinalizeTest {
    private static FinalizeTest ft = null;
    public void info(){
        System.out.println("测试资源清理得finalize方法");
    }
    public static void main(String[] args) {
        //创建FinalizeTest对象立即进入可恢复状态
        new FinalizeTest();
        //通知系统进行垃圾回收
        System.gc();
        //强制回收机制调用可恢复对象的finalize()方法
//        Runtime.getRuntime().runFinalization();
        System.runFinalization();
        ft.info();
    }
    @Override
    public void finalize(){
        //让ft引用到试图回收的可恢复对象,即可恢复对象重新变成可达
        ft = this;
        throw new RuntimeException("出异常了,你管不管啊");
    }
}
//输出结果
//测试资源清理得finalize方法
Nach dem Login kopieren

我们看到,finalize()方法将可恢复对象置为了可达对象,并且在finalize中抛出异常,都没有任何信息,被忽略了。

1.8.1、对象在内存中的状态

对象在内存中存在三种状态:

可达状态:有引用指向,这种对象为可达状态;

可恢复状态:失去引用,这种对象称为可恢复状态;垃圾回收机制开始回收时,回调用可恢复状态对象的finalize()方法(如果此方法让此对象重新获得引用,就会变为可达状态,否则,会变为不可大状态)。

不可达状态:彻底失去引用,这种状态称为不可达状态,如果垃圾回收机制这时开始回收,就会将这种状态的对象回收掉。

1.8.2、垃圾回收机制

垃圾回收机制只负责回收堆内存种的对象,不会回收任何物理资源(例如数据库连接、网络IO等资源);

程序无法精确控制垃圾回收的运行,垃圾回收只会在合适的时候进行。当对象为不可达状态时,系统会在合适的时候回收它的内存。

在垃圾回收机制回收任何对象之前,总会先调用它的finalize()方法,该方法可能会将对象置为可达状态,导致垃圾回收机制取消回收。

1.8.3、强制垃圾回收

上面我们已经说了,当对象失去引用时,会变为可恢复状态,但垃圾回收机制什么时候运行,什么时候调用finalize方法无法知道。虽然垃圾回收机制无法精准控制,但java还是提供了方法可以建议JVM进行垃圾回收,至于是否回收,这取决于虚拟机。但似乎可以看到一些效果。

public class GcTest {
    public static void main(String[] args){
        for(int i=0;i<4;i++){
            //没有引用指向这些对象,所以为可恢复状态
            new GcTest();
            //强制JVM进行垃圾回收(这只是建议JVM)
            System.gc();
            //Runtime.getRuntime().gc();
        }
    }
    @Override
    public void finalize(){
        System.out.println("系统正在清理GcTest资源。。。。");
    }
}
//输出结果
//系统正在清理GcTest资源。。。。
//系统正在清理GcTest资源。。。。
Nach dem Login kopieren

System.gc(),Runtime.getRuntime().gc()两个方法作用一样的,都是建议JVM垃圾回收,但不一定回收,多运行几次,结果可能都不一致。

Das obige ist der detaillierte Inhalt vonWas sind die Objektmethoden in Java?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Verwandte Etiketten:
Quelle:php.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage