Pour ceux qui entendent le mot Java Core pour la première fois, ce sont les principes fondamentaux du langage. Avec ces connaissances, vous pouvez partir en toute sécurité pour un stage/stage.
Ces questions vous aideront à rafraîchir vos connaissances avant l'entretien ou à apprendre quelque chose de nouveau par vous-même. Pour acquérir des compétences pratiques, étudiez à JavaRush . Article original Liens vers d'autres parties : Java Core. Questions d'entretien, partie 1 Java Core. Questions pour un entretien, partie 3
Pourquoi la méthode finalize() devrait-elle être évitée ?
Nous connaissons tous l'affirmation selon laquelle une méthodefinalize()
est appelée par le garbage collector avant de libérer la mémoire occupée par un objet. Voici un exemple de programme qui prouve qu'un appel de méthode finalize()
n'est pas garanti :
public class TryCatchFinallyTest implements Runnable {
private void testMethod() throws InterruptedException
{
try
{
System.out.println("In try block");
throw new NullPointerException();
}
catch(NullPointerException npe)
{
System.out.println("In catch block");
}
finally
{
System.out.println("In finally block");
}
}
@Override
protected void finalize() throws Throwable {
System.out.println("In finalize block");
super.finalize();
}
@Override
public void run() {
try {
testMethod();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class TestMain
{
@SuppressWarnings("deprecation")
public static void main(String[] args) {
for(int i=1;i< =3;i++)
{
new Thread(new TryCatchFinallyTest()).start();
}
}
}
Sortie : Dans le bloc try Dans le bloc catch Dans le bloc finalement Dans le bloc try Dans le bloc catch Dans le bloc finalement Dans le bloc try Dans le bloc catch Dans le bloc finalement Étonnamment, la méthode finalize
n'a été exécutée pour aucun thread. Cela prouve mes propos. Je pense que la raison est que les finaliseurs sont exécutés par un thread de garbage collector distinct. Si la machine virtuelle Java se termine trop tôt, le garbage collector n'a pas suffisamment de temps pour créer et exécuter les finaliseurs. D’autres raisons de ne pas utiliser la méthode finalize()
peuvent être :
- La méthode
finalize()
ne fonctionne pas avec des chaînes comme les constructeurs. Cela signifie que lorsque vous appelez un constructeur de classe, les constructeurs de superclasse seront appelés sans condition. Mais dans le cas de la méthodefinalize()
, cela n’arrivera pas. La méthode superclassefinalize()
doit être appelée explicitement. - Toute exception levée par la méthode
finalize
est ignorée par le thread du ramasse-miettes et ne sera pas propagée davantage, ce qui signifie que l'événement ne sera pas enregistré dans vos journaux. C'est très mauvais, n'est-ce pas ? -
Vous obtenez également une pénalité de performances significative si la méthode
finalize()
est présente dans votre classe. Dans Effective Programming (2e éd.), Joshua Bloch a déclaré :
« Oui, et encore une chose : il y a une grosse pénalité en termes de performances lors de l'utilisation de finaliseurs. Sur ma machine, le temps nécessaire pour créer et détruire des objets simples est d'environ 5,6 nanosecondes.
L'ajout d'un finaliseur augmente le temps à 2 400 nanosecondes. En d’autres termes, il est environ 430 fois plus lent de créer et de supprimer un objet avec un finaliseur.
Pourquoi HashMap ne devrait-il pas être utilisé dans un environnement multithread ? Cela pourrait-il provoquer une boucle infinie ?
Nous savons qu'ilHashMap
s'agit d'une collection non synchronisée dont la contrepartie synchronisée est HashTable
. Ainsi, lorsque vous accédez à une collection et dans un environnement multithread où tous les threads ont accès à une seule instance de la collection, il est alors plus sûr de l'utiliser HashTable
pour des raisons évidentes, comme éviter les lectures sales et garantir la cohérence des données. Dans le pire des cas, cet environnement multithread provoquera une boucle infinie. Oui c'est vrai. HashMap.get()
peut provoquer une boucle infinie. Voyons comment ? Si vous regardez le code source de la méthode HashMap.get(Object key)
, cela ressemble à ceci :
public Object get(Object key) {
Object k = maskNull(key);
int hash = hash(k);
int i = indexFor(hash, table.length);
Entry e = table[i];
while (true) {
if (e == null)
return e;
if (e.hash == hash && eq(k, e.key))
return e.value;
e = e.next;
}
}
while(true)
peut toujours être victime d'une boucle infinie dans un environnement d'exécution multithread si, pour une raison quelconque, e.next
il peut pointer vers lui-même. Cela provoquera une boucle sans fin, mais comment e.next
pointera-t-il vers lui-même (c'est-à-dire vers e
) ? Cela peut se produire dans une méthode void transfer(Entry[] newTable)
appelée lors de son HashMap
redimensionnement.
do {
Entry next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
Ce morceau de code a tendance à créer une boucle infinie si le redimensionnement se produit en même temps qu'un autre thread tente de modifier l'instance de la carte ( HashMap
). La seule façon d'éviter ce scénario est d'utiliser la synchronisation dans votre code, ou mieux encore, d'utiliser une collection synchronisée.
Expliquez l'abstraction et l'encapsulation. Comment sont-ils connectés ?
En termes simples , « L'abstraction affiche uniquement les propriétés d'un objet qui sont significatives pour la vue actuelle . » Dans la théorie de la programmation orientée objet, l'abstraction implique la capacité de définir des objets qui représentent des « acteurs » abstraits qui peuvent effectuer un travail, modifier et signaler des changements dans leur état et « interagir » avec d'autres objets du système. L'abstraction dans n'importe quel langage de programmation fonctionne de plusieurs manières. Cela se voit à la création de routines pour définir des interfaces pour les commandes de langage de bas niveau. Certaines abstractions tentent de limiter l'étendue de la représentation globale des besoins d'un programmeur en masquant complètement les abstractions sur lesquelles elles sont construites, comme les modèles de conception. Généralement, l'abstraction peut être vue de deux manières : L' abstraction de données est un moyen de créer des types de données complexes et d'exposer uniquement les opérations significatives pour interagir avec le modèle de données, tout en cachant tous les détails d'implémentation au monde extérieur. L'abstraction d'exécution est le processus d'identification de toutes les déclarations significatives et de leur exposition en tant qu'unité de travail. Nous utilisons généralement cette fonctionnalité lorsque nous créons une méthode pour effectuer un travail. Le confinement des données et des méthodes au sein des classes en combinaison avec le masquage (à l’aide du contrôle d’accès) est souvent appelé encapsulation. Le résultat est un type de données avec des caractéristiques et un comportement. L'encapsulation implique essentiellement également le masquage des données et le masquage de l'implémentation. "Encapsulez tout ce qui peut changer" . Cette citation est un principe de conception bien connu. D’ailleurs, dans n’importe quelle classe, des modifications de données peuvent survenir au moment de l’exécution et des modifications d’implémentation peuvent survenir dans les versions futures. Ainsi, l'encapsulation s'applique à la fois aux données et à l'implémentation. Ils peuvent donc être connectés comme ceci :- L'abstraction est principalement ce qu'une classe peut faire [Idée]
- L'encapsulation est plus Comment réaliser cette fonctionnalité [Mise en œuvre]
Différences entre interface et classe abstraite ?
Les principales différences peuvent être répertoriées comme suit :- Une interface ne peut implémenter aucune méthode, mais une classe abstraite le peut.
- Une classe peut implémenter plusieurs interfaces, mais ne peut avoir qu'une seule superclasse (abstraite ou non abstraite)
- Une interface ne fait pas partie d'une hiérarchie de classes. Des classes non liées peuvent implémenter la même interface.
Cat
et Dog
peut hériter de la classe abstraite Animal
, et cette classe de base abstraite implémentera la méthode void Breathe()
- Breathe, que tous les animaux exécuteront ainsi de la même manière. Quels verbes peuvent être appliqués à ma classe et peuvent être appliqués aux autres ? Créez une interface pour chacun de ces verbes. Par exemple, tous les animaux peuvent manger, je vais donc créer une interface IFeedable
et lui faire Animal
implémenter cette interface. Juste assez bon pour implémenter une interface Dog
( capable de m'aimer), mais pas toutes. Quelqu'un a dit : la principale différence réside dans l'endroit où vous souhaitez être mis en œuvre. Lorsque vous créez une interface, vous pouvez déplacer l'implémentation vers n'importe quelle classe qui implémente votre interface. En créant une classe abstraite, vous pouvez partager l'implémentation de toutes les classes dérivées en un seul endroit et éviter bien des problèmes comme la duplication de code. Horse
ILikeable
Comment StringBuffer économise-t-il la mémoire ?
La classeString
est implémentée comme un objet immuable, ce qui signifie que lorsque vous décidez initialement de mettre quelque chose dans l'objet String
, la machine virtuelle alloue un tableau de longueur fixe exactement de la taille de votre valeur d'origine. Celle-ci sera ensuite traitée comme une constante à l'intérieur de la machine virtuelle, ce qui apporte une amélioration significative des performances si la valeur de la chaîne ne change pas. Cependant, si vous décidez de modifier le contenu d'une chaîne de quelque manière que ce soit, la machine virtuelle fait en réalité copier le contenu de la chaîne d'origine dans un espace temporaire, apporter vos modifications, puis enregistrer ces modifications dans une nouvelle matrice mémoire. Ainsi, apporter des modifications à la valeur d’une chaîne après l’initialisation est une opération coûteuse. StringBuffer
, d'autre part, est implémenté sous la forme d'un tableau à expansion dynamique à l'intérieur de la machine virtuelle, ce qui signifie que toute opération de modification peut avoir lieu sur une cellule de mémoire existante et qu'une nouvelle mémoire sera allouée selon les besoins. Cependant, la machine virtuelle n'a aucun moyen d'effectuer l'optimisation StringBuffer
car son contenu est considéré comme incohérent dans chaque instance.
Pourquoi les méthodes wait et notify sont-elles déclarées dans la classe Object au lieu de Thread ?
Les méthodeswait
, notify
, notifyAll
ne sont nécessaires que lorsque vous souhaitez que vos threads aient accès aux ressources partagées et que la ressource partagée peut être n'importe quel objet Java du tas. Ainsi, ces méthodes sont définies sur la classe de base Object
afin que chaque objet dispose d'un contrôle qui permet aux threads d'attendre sur leur moniteur. Java n'a aucun objet spécial utilisé pour partager une ressource partagée. Aucune structure de données de ce type n’est définie. Par conséquent, il est de la responsabilité de la classe Object
de pouvoir devenir une ressource partagée et de fournir des méthodes d'assistance telles que wait()
, notify()
, notifyAll()
. Java est basé sur l'idée des moniteurs de Charles Hoare. En Java, tous les objets ont un moniteur. Les threads attendent sur les moniteurs, donc pour effectuer l'attente, nous avons besoin de deux paramètres :
- un fil
- surveiller (n’importe quel objet).
wait
). C'est une bonne conception car si nous pouvons forcer n'importe quel autre thread à attendre sur un moniteur spécifique, cela entraînera une "invasion", rendant difficile la conception/programmation de programmes parallèles. N'oubliez pas qu'en Java, toutes les opérations qui interfèrent avec d'autres threads sont obsolètes (par exemple, stop()
).
Écrivez un programme pour créer une impasse en Java et corrigez-la
En Javadeadlock
, il s'agit d'une situation dans laquelle au moins deux threads détiennent un bloc sur des ressources différentes et tous deux attendent que l'autre ressource soit disponible pour terminer leur tâche. Et aucun d’entre eux n’est capable de verrouiller la ressource détenue. Exemple de programme :
package thread;
public class ResolveDeadLockTest {
public static void main(String[] args) {
ResolveDeadLockTest test = new ResolveDeadLockTest();
final A a = test.new A();
final B b = test.new B();
// Thread-1
Runnable block1 = new Runnable() {
public void run() {
synchronized (a) {
try {
// Добавляем задержку, чтобы обе нити могли начать попытки
// блокирования ресурсов
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Thread-1 заняла A но также нуждается в B
synchronized (b) {
System.out.println("In block 1");
}
}
}
};
// Thread-2
Runnable block2 = new Runnable() {
public void run() {
synchronized (b) {
// Thread-2 заняла B но также нуждается в A
synchronized (a) {
System.out.println("In block 2");
}
}
}
};
new Thread(block1).start();
new Thread(block2).start();
}
// Resource A
private class A {
private int i = 10;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
}
// Resource B
private class B {
private int i = 20;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
}
}
L'exécution du code ci-dessus entraînera un blocage pour des raisons très évidentes (expliquées ci-dessus). Nous devons maintenant résoudre ce problème. Je crois que la solution à tout problème se trouve à la racine du problème lui-même. Dans notre cas, le modèle d’accès à A et B constitue le principal problème. Par conséquent, pour le résoudre, nous changeons simplement l’ordre des opérateurs d’accès aux ressources partagées. Après le changement, cela ressemblera à ceci :
// Thread-1
Runnable block1 = new Runnable() {
public void run() {
synchronized (b) {
try {
// Добавляем задержку, чтобы обе нити могли начать попытки
// блокирования ресурсов
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Thread-1 заняла B но также нуждается в А
synchronized (a) {
System.out.println("In block 1");
}
}
}
};
// Thread-2
Runnable block2 = new Runnable() {
public void run() {
synchronized (b) {
// Thread-2 заняла B но также нуждается в А
synchronized (a) {
System.out.println("In block 2");
}
}
}
};
Exécutez à nouveau cette classe et vous ne verrez plus l'impasse. J'espère que cela vous aidera à éviter les blocages et à vous en débarrasser si vous les rencontrez.
Que se passe-t-il si votre classe qui implémente l'interface Serialisable contient un composant non sérialisable ? Comment régler ceci?
Dans ce cas, il sera lancéNotSerializableException
lors de l'exécution. Pour résoudre ce problème, il existe une solution très simple : cochez ces cases transient
. Cela signifie que les champs cochés ne seront pas sérialisés. Si vous souhaitez également stocker l'état de ces champs, vous devez alors prendre en compte les variables de référence, qui implémentent déjà le Serializable
. Vous devrez peut-être également utiliser les méthodes readResolve()
et writeResolve()
. Résumons :
- Tout d'abord, rendez votre champ non sérialisable
transient
. - Tout d’abord
writeObject
, appelezdefaultWriteObject
le thread pour enregistrer tous les non-transient
champs, puis appelez les méthodes restantes pour sérialiser les propriétés individuelles de votre objet non sérialisable. - Dans
readObject
, appelez d'aborddefaultReadObject
le flux pour lire tous les non-transient
champs, puis appelez d'autres méthodes (correspondant à celles que vous avez ajoutées danswriteObject
) pour désérialiser votre non-transient
objet.
Expliquer les mots-clés transitoires et volatils en Java
"Le mot-clétransient
est utilisé pour indiquer les champs qui ne seront pas sérialisés." Selon la spécification du langage Java : les variables peuvent être marquées avec l'indicateur transitoire pour indiquer qu'elles ne font pas partie de l'état persistant de l'objet. Par exemple, vous pouvez contenir des champs dérivés d'autres champs et il est préférable de les obtenir par programme plutôt que de restaurer leur état via la sérialisation. Par exemple, dans une classe, BankPayment.java
les champs tels que principal
(directeur) et rate
(taux) peuvent être sérialisés et interest
(intérêts courus) peuvent être calculés à tout moment, même après la désérialisation. Si l’on s’en souvient, chaque thread en Java possède sa propre mémoire locale et effectue des opérations de lecture/écriture sur cette mémoire locale. Lorsque toutes les opérations sont terminées, il écrit l'état modifié de la variable dans la mémoire partagée, à partir de laquelle tous les threads accèdent à la variable. En règle générale, il s'agit d'un thread normal à l'intérieur d'une machine virtuelle. Mais le modificateur volatile indique à la machine virtuelle que l'accès d'un thread à cette variable doit toujours faire correspondre sa propre copie de cette variable avec la copie principale de la variable en mémoire. Cela signifie que chaque fois qu'un thread souhaite lire l'état d'une variable, il doit effacer l'état de la mémoire interne et mettre à jour la variable depuis la mémoire principale. Volatile
le plus utile dans les algorithmes sans verrouillage. Vous marquez une variable stockant des données partagées comme volatile, vous n'utilisez alors pas de verrous pour accéder à cette variable et toutes les modifications apportées par un thread seront visibles par les autres. Ou si vous souhaitez créer une relation « arrivé après » pour garantir que les calculs ne sont pas répétés, encore une fois pour garantir que les changements sont visibles en temps réel. Volatile doit être utilisé pour publier en toute sécurité des objets immuables dans un environnement multithread. La déclaration de champ public volatile ImmutableObject
garantit que tous les threads voient toujours la référence actuellement disponible à l'instance.
Différence entre Itérateur et ListIterator ?
Nous pouvons utiliser , ouIterator
parcourir les éléments . Mais il ne peut être utilisé que pour parcourir des éléments . D'autres différences sont décrites ci-dessous. Tu peux: Set
List
Map
ListIterator
List
- itérer dans l’ordre inverse.
- obtenir l'index n'importe où.
- ajoutez n’importe quelle valeur n’importe où.
- définir n’importe quelle valeur à la position actuelle.
GO TO FULL VERSION